大家好,我是北军。synchronized在我们的日常工作中也是非常常用的,对于摆脱多线程的困扰有很大的帮助。但是如果synchronized使用不当,可能会给我们带来很大的麻烦。在本文中,我们将讨论与同步相关的一些不良做法,以及针对每个用例的更好方法。同步原则一般来说,我们应该只同步我们确信没有外部代码会锁定的对象。换句话说,使用池化或可重用对象进行同步是一种不好的做法。原因是池化/可重用对象可以被JVM中的其他进程访问,并且外部/不受信任的代码对这些对象的任何修改都可能导致死锁和非确定性行为。现在,让我们讨论一下基于String、Boolean、Integer和Object等类型的同步原理。Stringliterals1.不正确的用法Stringliterals有一个池,在Java中经常被重用。所以不推荐使用String类型用synchronized关键字进行同步。publicvoidstringBadPractice1(){StringstringLock="LOCK_STRING";synchronized(stringLock){//...}}同样,如果我们使用私有的finalString文字,它仍然从常量池中引用。privatefinalStringstringLock="LOCK_STRING";publicvoidstringBadPractice2(){synchronized(stringLock){//...}}此外,为了同步而写入字符串被认为是不好的做法。privatefinalStringinternedStringLock=newString("LOCK_STRING").intern();publicvoidstringBadPractice3(){synchronized(internedStringLock){//...}}根据Javadocs,intern方法为我们提供了String对象的规范表示。换句话说,intern方法从池中返回一个String——如果它不在池中,则显式地将其添加到池中——其内容与该String相同。因此,内部String对象也存在可重用对象上的同步问题。注意:所有取String值的String字面量和常量表达式都是自动实现的。2.正确使用为了避免在String字面量上进行同步的不良做法,建议使用new关键字来创建一个新的String实例。让我们在已经讨论过的代码中解决这个问题。首先,我们将创建一个新的String对象,使其具有唯一引用(以避免任何重用)和它自己的内在锁,这有助于同步。然后我们将该对象保持为私有和最终状态,以防止任何外部/不受信任的代码访问它。privatefinalStringstringLock=newString("LOCK_STRING");publicvoidstringSolution(){synchronized(stringLock){//...}}Boolean字面量Boolean类型有两个值,即true和false,故意不适合Lock。与JVM中的字符串文字类似,布尔文字也共享Boolean类的唯一实例。让我们看一个在布尔锁对象上错误使用同步的例子。privatefinalBooleanbooleanLock=Boolean.FALSE;publicvoidbooleanBadPractice(){synchronized(booleanLock){//...}}在这里,如果任何外部代码也同步了具有相同值的布尔文字,系统将变得无响应,或导致死锁情况。因此,我们不推荐使用布尔对象作为同步锁。基本类型的包装类1.错误使用与布尔字段类似,基本类型的包装类可能会重用某些值的实例。原因是JVM缓存和共享可以表示为字节的值。例如,让我们编写一个在Integer上进行同步的错误用法示例。privateintcount=0;privatefinalIntegerintLock=count;publicvoidboxedPrimitiveBadPractice(){synchronized(intLock){count++;//...}}2.正确用法然而,与booleanliterals不同的是,在原始类型包装类上同步的解决方案是创建一个新的实例。与String对象类似,我们应该使用new关键字来创建Integer对象的唯一实例,该实例具有自己的内部锁并保持私有和最终状态。私人整数计数=0;privatefinalIntegerintLock=newInteger(count);publicvoidboxedPrimitiveSolution(){synchronized(intLock){count++;//...}}当一个类使用this关键字来实现方法同步或块同步时,JVM使用对象本身作为监视器(它的内在锁)。不受信任的代码可以获取并无限期地持有可访问类的内部锁。因此,这可能会导致死锁情况。1.不正确的用法例如,让我们创建一个Animal类,它有一个同步方法setName和一个方法setOwnerwithsynchronizedblock。公共类动物{私有字符串名称;私有字符串所有者;//getter和构造函数publicsynchronizedvoidsetName(Stringname){this.name=name;}publicvoidsetOwner(Stringowner){synchronized(this){this.owner=owner;}}}现在,让我们写一些误用,创建一个Animal类的实例,并同步它。AnimalanimalObj=newAnimal("Tommy","John");synchronized(animalObj){while(true){Thread.sleep(Integer.MAX_VALUE);}}这里,不受信任的代码示例引入了一个无限延迟,阻止setName和setOwner方法的实现获取相同的锁。2、正确使用防止该漏洞的解决方案是私有锁对象。这个想法是使用与我们类中定义的Object类的私有最终实例关联的内部锁,而不是对象本身的内部锁。此外,我们应该使用块同步而不是方法同步来增加灵活性并将未同步的代码排除在块之外。因此,让我们对Animal类进行必要的更改。publicclassAnimal{//...privatefinalObjectobjLock1=newObject();privatefinalObjectobjLock2=newObject();publicvoidsetName(Stringname){synchronized(objLock1){this.name=name;}}publicvoidsetOwner(Stringowner){synchronized(objLock2){this.owner=owner;}}}这里,为了提高并发性,我们通过定义多个privatefinallock对象来细化锁定方案,以将我们从两个方法——setName和setOwner的两个Synchronous关注点中分离出来。此外,如果实现同步块的方法修改静态变量,我们必须通过锁定静态对象来实现同步。privatestaticintstaticCount=0;privatestaticfinalObjectstaticObjLock=newObject();publicvoidstaticVariableSolution(){synchronized(staticObjLock){count++;//...}}与某些类型(例如字符串、布尔值、整数和对象)的同步相关的不良做法。本文最重要的收获是不建议使用池或可重用对象进行同步。此外,建议在Object类的私有最终实例上进行同步。外部/不受信任的代码将无法访问此类对象,否则这些代码可能会与我们的公共类进行交互,从而减少此类交互导致死锁的可能性。
