随着多核时代的到来,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。它基于乐观锁,通过循环的方式来不断轮询看是不是有线程更新了这个值。
一般我们的场景都是IO密集型运算, 所以才可以做一些并发编程的优化。要是CPU密集型的话, CPU一直很忙, 那就没有优化余地了。AUTOMIC这个包也是基于这个考虑才使用乐观并发的方式的。