1.概述
Java中的同步对于摆脱多线程问题很有帮助。但是,如果不仔细使用同步原理,可能会给我们带来很多麻烦。
在本教程中,我们将讨论与同步相关的一些不良做法以及每种用例的更好方法。
2.同步原理
通常,我们应该仅在确定没有外部代码锁定的对像上进行同步。
换句话说,使用池化或可重用的对象进行同步是一种不好的做法。原因是JVM中的其他进程可以访问池化/可重用的对象,并且外部/不受信任的代码对此类对象进行的任何修改都可能导致死锁和不确定性行为。
现在,让我们讨论基于某些类型的同步原理,例如String
, Boolean
, Integer
和Object
。
3. String
字符串
3.1 不良做法
字符串文字被合并,并经常在Java中重用。因此,建议不要将String
类型与synchronized
关键字一起使用来进行同步:
public void stringBadPractice1() {
String stringLock = "LOCK_STRING";
synchronized (stringLock) {
// ...
}
}
同样,如果我们使用private final String
文字,则仍从常量池中引用它:
private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
synchronized (stringLock) {
// ...
}
}
此外,它被认为是不好的做法, intern
的String
进行同步:
private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
synchronized (internedStringLock) {
// ...
}
}
根据Javadocs , intern
方法为我们提供String
对象的规范表示。换句话说, intern
方法从池中返回一个String
,并将其显式添加到池中(如果不存在),其内容与此String
相同。
因此,对于可重复使用的对象,同步问题对于持久化的String
像也仍然存在。
注意:所有String
文字和字符串值常量表达式都将自动被interned 。
3.2 解决办法
建议避免对String
文字进行同步的不良做法,建议使用new
关键字创建String
的新实例。
让我们用已经讨论过的代码解决问题。首先,我们将创建一个新的String
对象,使其具有唯一的引用(以避免任何重用)和它自己的固有锁定,这有助于同步。
然后,我们将对象保持private
和final
以防止任何外部/不受信任的代码访问它:
private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
synchronized (stringLock) {
// ...
}
}
4. Boolean
类型
具有两个值true
和false
的Boolean
类型不适用于锁定目的。类似于JVM中的String
文字, boolean
文字值还共享Boolean
类的唯一实例。
让我们看一个错误的代码示例,该示例在Boolean
锁定对像上进行同步:
private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
synchronized (booleanLock) {
// ...
}
}
在这里,如果任何外部代码也在具有相同值的Boolean
文字上同步,则系统可能变得无响应或导致死锁情况。
因此,我们不建议将Boolean
对像用作同步锁。
5.装箱基本类型
5.1。不良做法
类似于boolean
文字,装箱的类型可能会将实例重用于某些值。原因是JVM缓存并共享可以表示为字节的值。
例如,让我们写一个糟糕的代码示例,对盒装类型Integer
同步:
private int count = 0;
private final Integer intLock = count;
public void boxedPrimitiveBadPractice() {
synchronized (intLock) {
count++;
// ...
}
}
5.2 解决办法
但是,与boolean
文字不同,对盒装原语进行同步的解决方案是创建一个新实例。
与String
像类似,我们应该使用new
关键字创建具有自己的内部锁的Integer
对象的唯一实例,并将其保持private
和final
:
private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
synchronized (intLock) {
count++;
// ...
}
}
6.this同步
当类使用this
关键字实现方法同步或阻止同步时,JVM将对象本身用作监视器(其内部锁)。
不受信任的代码可以获取并无限期持有可访问类的内部锁。因此,这可能导致死锁情况。
6.1。不良做法
例如,让我们使用synchronized
方法setName
和带有synchronized
块的方法setOwner
创建Animal
类:
public class Animal {
private String name;
private String owner;
// getters and constructors
public synchronized void setName(String name) {
this.name = name;
}
public void setOwner(String owner) {
synchronized (this) {
this.owner = owner;
}
}
}
现在,让我们编写一些错误的代码来创建Animal
类的实例并对其进行同步:
Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
while(true) {
Thread.sleep(Integer.MAX_VALUE);
}
}
在这里,不受信任的代码示例引入了不确定的延迟,从而阻止了setName
和setOwner
方法的实现获取相同的锁。
6.2 解决办法
防止此漏洞的解决方案是私有锁对象。
这个想法是使用与在我们的类中定义的Object
类的**private final
实例相关联的内在锁来代替对象本身的内在锁**。
另外,我们应该使用块同步来代替方法同步,以增加灵活性,以将异步代码排除在块外。
因此,让我们对Animal
类进行必要的更改:
public class Animal {
// ...
private final Object objLock1 = new Object();
private final Object objLock2 = new Object();
public void setName(String name) {
synchronized (objLock1) {
this.name = name;
}
}
public void setOwner(String owner) {
synchronized (objLock2) {
this.owner = owner;
}
}
}
在这里,为了更好的并发性,我们通过定义多个private final
锁定对象来细化锁定方案,以分离我们对两个方法setName
和setOwner
同步关注。
此外,如果实现synchronized
块的方法修改了static
变量,则必须通过锁定static
对象来进行同步:
private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
synchronized (staticObjLock) {
count++;
// ...
}
}
7.结论
在本文中,我们讨论了与某些类型(如String
, Boolean
, Integer
和Object
上的同步相关的一些不良做法。
本文最重要的要点是,不建议使用池化或可重用的对象进行同步。
另外,建议在Object
类的private final
实例上进行同步。外部/不受信任的代码将无法访问此对象,否则这些代码可能会与我们的public
类进行交互,从而减少了此类交互可能导致死锁的可能性。
0 评论