Java多线程并发编程 并发三大要素
一、原子性
原子,一个不可再被分割的颗粒。原子性,指的是一个或多个不能再被分割的操作。
inti=1;//原子操作
i++;//非原子操作,从主内存读取i到线程工作内存,进行+1,再把i写到朱内存。
虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为“复合操作”。
我们可以用synchronized或Lock来把这个复合操作“变成”原子操作。
例子:
privatesynchronizedvoidincrease(){ i++; }
或
privateinti=0; LockmLock=newReentrantLock(); privatevoidincrease(){ mLock.lock(); try{ i++; }finally{ mLock.unlock(); } }
这样我们就可以把这个一个方法看做一个整体,一个不可分割的整体。
除此之前,我们还可以用java.util.concurrent.atomic里的原子变量类,可以确保所有对计数器状态访问的操作都是原子的。
例子:
AtomicIntegermAtomicInteger=newAtomicInteger(0); privatevoidincrease(){ mAtomicInteger.incrementAndGet(); }
二、可见性
当多线程访问某一个(同一个)变量时,其中一条线程对此变量作出修改,其他线程可以立刻读取到最新修改后的变量。
inti=0; //线程1执行 i++; //线程2执行 System.out.print("i="+i);
即使是在执行完线程里的i++后再执行线程2,线程2的输入结果也会有2个种情况,一个是0和1。
因为i++在线程1(CPU1)中做完了运算,并没有立刻更新到主内存当中,而线程2(CPU2)就去主内存当中读取并打印,此时打印的就是0。
synchronized和Lock能够保证可见性。
另外volatile关键字也可以解决这个问题(下一篇会讲到)。
三、有序性
我们都知道处理器为了拥有更好的运算效率,会自动优化、排序执行我们写的代码,但会确保执行结果不变。
例子:
inta=0;//语句1 intb=0;//语句2 i++;//语句3 b++;//语句4
这一段代码的执行顺序很有可能不是按上面的1、2、3、4来依次执行,因为1和2没有数据依赖,3和4没有数据依赖,2、1、4、3这样来执行可以吗?完全没问题,处理器会自动帮我们排序。
在单线程看来并没有什么问题,但在多线程则很容易出现问题。
再来个例子:
//线程1 init(); inited=true; //线程2 while(inited){ work(); }
init();与inited=true;并没有数据的依赖,在单线程看来,如果把两句的代码调换好像也不会出现问题。
但此时处于一个多线程的环境,而处理器真的把这两句代码重新排序,那问题就出现了,若线程1先执行inited=true;此时,init()并没有执行,线程2就已经开始调用work()方法,此时很可能造成一些奔溃或其他BUG的出现。
synchronized和Lock能确保原子性,能让多线程执行代码的时候依次按顺序执行,自然就具有有序性。
而volatile关键字也可以解决这个问题,volatile关键字可以保证有序性,让处理器不会把这行代码进行优化排序。