随着多核时代的到来,JAVA类库提供了更多的并发方面的处理,这里结合《Effective Java》做个总结:
1. 区分线程操作是并发还是为了通讯,不仅仅是并发的情况需要同步。
JAVA 对于32位以下(依赖于硬件)可以表示的类型,也就是除了double和long的,都是可以通过原子操作完成的,但是当一个线程改变了这个变量时,并不立即在另外一个线程里可以看到,这依赖于线程的通讯。
看如下例子:
/** * @description 如下这种写法,在我的虚拟机上可以正常停止,但是如果虚拟机做过优化,则不一定能正确结束 * @author job * @date 2010-8-16 下午08:48:25 * @version 1.0 */ public class StopThread { public static Boolean stopRequest = Boolean.FALSE; public static void main(String[] args) throws InterruptedException { Thread backThread = new Thread(new Runnable() { public void run() { int i= 0; long start = System.nanoTime(); while(!stopRequest){ System.out.println(i); i++; } long totalTime = System.nanoTime()-start; System.out.println("time:"+totalTime); } }); backThread.start(); TimeUnit.SECONDS.sleep(1); stopRequest = true; } }
运行结果为 time:997494594
可以看到在我的虚拟机上可以正常的通讯,但是在有些虚拟机上会将循环代码优化为
if(!stopRequest){
while(true)
i++;
}
这个就叫活性失败。为什么主线程和子线程需要通讯呢,这个和JMM (JAVA的内存模型)有关,JAVA给不同的线程分配了不同的内存,叫工作内存,工作内存之间互相使不可见的,只能通过主存进行通讯。可以有如下两种方法来进行同步
2. synchronized 同步,这个是在不同线程之间通过 wait 和notify事件进行同步,效率不高。
class SyncStopThread { public static Boolean stopRequest = false; private static synchronized void requestStop(){ stopRequest= true; } private static synchronized boolean stopRequest(){ return stopRequest; } public static void main(String[] args) throws InterruptedException { Thread backThread = new Thread(new Runnable() { public void run() { int i= 0; long start = System.nanoTime(); while(!stopRequest()){ System.out.println(i); i++; } long totalTime = System.nanoTime()-start; System.out.println("time:"+totalTime); } }); backThread.start(); TimeUnit.SECONDS.sleep(1); requestStop() ; } }
3. 使用volatile 进行不稳定变量的声明。
volatile的语义, 其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我.因此, 当多核或多线程在访问该变量时, 都将直接操作主存, 这从本质上, 做到了变量共享.效率更高
class VolatileStopThread { public static volatile Boolean stopRequest = false; public static void main(String[] args) throws InterruptedException { Thread backThread = new Thread(new Runnable() { public void run() { int i= 0; long start = System.nanoTime(); while(!stopRequest){ System.out.println(i); i++; } long totalTime = System.nanoTime()-start; System.out.println("time:"+totalTime); } }); backThread.start(); TimeUnit.SECONDS.sleep(1); stopRequest =true ; } }
注意:volatile 只对原子性的操作起作用,如果是序列号的增加 i++这种操作,是不能通过volatile 来避免并发问题的,两个线程有可能同时读到一个值,进行操作,然后获得了相同的序列号,这种就是 安全性失败。最好的方式是使用atomic的类库,这个下篇博客详细介绍。
4. 总结:对于线程之间共享变量,有几个原则可以借鉴
(1)尽量将可变变量保存在线程中
(2)线程安全通讯(安全发布)的几种方法:保存在静态域中,作为初始化的一部分;保存在volatile、final或者通过正常锁定的域中,也可以放到concurrentMap等并发集合中。
(3)基本上通过volatile 和 atomic 能够解决一般的并发问题。
(4)volatile 适用于仅仅需要通讯,不需要互相排斥操作的情况下:一般来说,子线程和主线程之间是需要通讯的,子线程和子线程对可变参数的处理都是要互相排斥的。
补充同事的一个关于volatile 和atomic的诠释:
volatile 只是将内容放入主存,也就是共享内存,而一般的如果不使用这个声明,那就是放入二级缓存,其它线程可能将这个变量已经更新到主存,就引起脏读,它不能解决 int 和long等占用两个字节的操作,说白了就是:volatile只保证线程同时对存储单元的可见性。但是不保证原子性。要实现原子性,就要使用AUTOMIC。它基于乐观锁,通过循环的方式来不断轮询看是不是有线程更新了这个值。