转自 https://blog.csdn.net/suifeng3051/article/details/52164267
本文链接:https://blog.csdn.net/suifeng3051/article/details/52164267
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
产生线程不安全的原因
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。1 public class Counter { 2 protected long count = 0; 3 public void add(long value){ 4 this.count = this.count + value; 5 } 6 }
想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:
1 从内存获取 this.count 的值放到寄存器 2 将寄存器中的值增加value 3 将寄存器中的值写回内存
观察线程A和B交错执行会发生什么:
1 this.count = 0; 2 A: 读取 this.count 到一个寄存器 (0) 3 B: 读取 this.count 到一个寄存器 (0) 4 B: 将寄存器的值加2 5 B: 回写寄存器值(2)到内存. this.count 现在等于 2 6 A: 将寄存器的值加3 7 A: 回写寄存器值(3)到内存. this.count 现在等于 3
两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:
1 public void someMethod(){ 2 long threadSafeInt = 0; 3 threadSafeInt++; 4 }
局部的对象引用
上面提到的局部变量是一个基本类型,如果局部变量是一个对象类型呢?对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内,所有的对象都存在共享堆中,所以对于局部对象的引用,有可能是线程安全的,也有可能是线程不安全的。1 public void someMethod(){ 2 LocalObject localObject = new LocalObject(); 3 localObject.callMethod(); 4 method2(localObject); 5 } 6 public void method2(LocalObject localObject){ 7 localObject.setValue("value"); 8 }
上面样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象,始终在someMethod()方法内部。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。
1 public class NotThreadSafe{ 2 StringBuilder builder = new StringBuilder(); 3 public add(String text){ 4 this.builder.append(text); 5 } 6 }
如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:
1 NotThreadSafe sharedInstance = new NotThreadSafe(); 2 new Thread(new MyRunnable(sharedInstance)).start(); 3 new Thread(new MyRunnable(sharedInstance)).start(); 4 public class MyRunnable implements Runnable{ 5 NotThreadSafe instance = null; 6 public MyRunnable(NotThreadSafe instance){ 7 this.instance = instance; 8 } 9 public void run(){ 10 this.instance.add("some text"); 11 } 12 }
注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件。
1 new Thread(new MyRunnable(new NotThreadSafe())).start(); 2 new Thread(new MyRunnable(new NotThreadSafe())).start();
现在两个线程都有自己单独的NotThreadSafe对象,访问的不是同一资源,不满足竞态条件,是线程安全的。所以非线程安全的对象仍可以通过某种方式来消除竞态条件。
判断资源对象是否是线程安全
线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。1 如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
资源可以是对象,数组,文件,数据库连接,套接字等等。Java中我们无需主动销毁对象,所以“销毁”指不再有引用指向对象。
1 检查记录X是否存在,如果不存在,插入X
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
1 线程1检查记录X是否存在。检查结果:不存在 2 线程2检查记录X是否存在。检查结果:不存在 3 线程1插入记录X 4 线程2插入记录X
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。
1 public class ImmutableValue{ 2 private int value = 0; 3 public ImmutableValue(int value){ 4 this.value = value; 5 } 6 public int getValue(){ 7 return this.value; 8 } 9 }
如果你需要对ImmutableValue类的实例进行操作,如添加一个类似于加法的操作,我们不能对这个实例直接进行操作,只能创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:
1 public class ImmutableValue{ 2 private int value = 0; 3 public ImmutableValue(int value){ 4 this.value = value; 5 } 6 public int getValue(){ 7 return this.value; 8 } 9 public ImmutableValue add(int valueToAdd){ 10 return new ImmutableValue(this.value + valueToAdd); 11 } 12 }
重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:
public void Calculator{ private ImmutableValue currentValue = null; public ImmutableValue getValue(){ return currentValue; } public void setValue(ImmutableValue newValue){ this.currentValue = newValue; } public void add(int newValue){ this.currentValue = this.currentValue.add(newValue); } }
在Java多线程编程当中,提供了多种实现Java线程安全的方式:
使用java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
使用java.util.concurrent.locks 包中的锁
使用线程安全的集合ConcurrentHashMap
使用volatile关键字,保证变量可见性(直接从内存读,而不是从线程cache读)
————————————————
版权声明:本文为CSDN博主「Heaven-Wang」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/suifeng3051/article/details/52164267