首先要知道什么是线程安全?
当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
典型线程不安全的列子:
1 import java.util.*; 4 5 class Worker implements Runnable { 6 private UnsafeCount unsafeCount; 7 8 public Worker(UnsafeCount unsafeCount) { 9 this.unsafeCount = unsafeCount; 10 } 11 12 @Override 13 public void run() { 14 // TODO Auto-generated method stub 15 for (int i = 0; i < 1000; i++) 16 unsafeCount.increase(); 17 } 18 19 } 20 21 public class UnsafeCount { 22 private int count = 0; 23 24 public void increase() { 25 count++; 26 } 27 28 public int getCount() { 29 return count; 30 } 31 32 public static void main(String[] args) throws InterruptedException { 33 UnsafeCount uc = new UnsafeCount(); 34 35 //这里用了list简陋的方式控制线程的结束,更好的实现是用闭锁CountDownLatch或者栅栏CyclicBarrier 36 List<Thread> list = new ArrayList<Thread>();// 37 38 for (int i = 0; i < 10; i++) { 39 Thread worker = new Thread(new Worker(uc)); 40 worker.start(); 41 list.add(worker); 42 } 43 44 //阻塞直到线程结束 45 for (Thread t : list) { 46 t.join(); 47 } 48 49 System.out.println("total is: " + uc.getCount()); 50 51 } 52 }
运行结果(每次结果都不一样):
total is: 7628
我们来仔细分析一下这个结果,开启10个线程运行,每个线程都对count进行了1000次自增操作,期望的结果应该是1000*10=10000。很明显运行结果与期望结果不一致。结论是这个类是线程不安全的。为什么会出现这种情况了?
原因是count++这个操作不是原子性,其实这个自增操作是个复合操作:读-改-写。 如果我们了解汇编语言的话,对应自增操作的汇编程序可能是:
movl count, %eax #将count的值读入eax的寄存器中, inc %eax #寄存器eax里的值加1,即改写count值 movl %eax, %ebx #这里ebx寄存器存存放着count的内存地址,这里是值将改写的count值写入到内存中
那么这样就存在一个问题,假如就存在2个线程A和B操作变量count,初始化时刻count为0. 在线程A未写入改写值之前,比如在A线程执行步骤2的时刻, 线程B开始执行,如下所示:
线程A读入count值为0 (步骤1) -》 改写count值为1(步骤2) -》 将改写后的count值写入内存中(步骤3)
因为线程A还没有更新改写count的值到内存,这时线程B读入count的值仍旧是0,导致最后2个线程结束后count的值为1。由此可见做了2次自增的操作,期望结果是2,但实际结果可能是1.这也是线程不安全的情况下,自增的操作的实际结果往往比期望结果小的原因。
下篇准备将讲什么情况是线程不安全的。