线程通信模型大体可分为两种,共享变量和消息传递。虽然消息传递(Actor模型)是比较被推崇的,但是Java语言并不支持。所以在并发程序中我们必须要面对共享变量所带来的编程复杂度。

所谓共享变量,即一个类的静态域或者实例域。先看一个线程不安全的例子。

public static void main(String[] args) {
	
	final Counter counter = new Counter();
	
	ExecutorService es = Executors.newFixedThreadPool(5);
	for(int i=0;i<5;i++){
		es.execute(new Runnable() {
			@Override
			public void run() {
				for(int i=0;i<10000;i++)
					counter.incr();
			}
		});
	}
	
	es.shutdown();
	while(true){
		if(es.isTerminated())
			break;
	}
	
	System.out.println("final cnt : " + counter.cnt);
	
}

private static class Counter{
	
	public int cnt = 0;
	
	public void incr(){
		cnt++;
	}
}

5个线程分别对一个变量进行自增操作1w次,结果并非是5w而是少了很多。原因可以看下Java线程的内存模型,每个线程都有自己的本地栈缓存了counter的副本,修改了以后需要刷新到堆中其他线程才能感知到变化。这过程并非是原子的。深入的原理可以参考下 <<深入理解Java内存模型>> http://ifeve.com/java-memory-model-0/

这边只对比下解决的方式

synchronized

最简单粗暴的方式就是同步原语,早期的Java提供的一种同步方式,在方法上面加上synchronized关键字或者方法块加上synchronized,这样做的话方法或者代码块保证了原子性,只有一个线程能同时访问。

// 修改后的方式
private static class Counter{
		
	private Object lock = new Object();
	
	public int cnt = 0;
	
	public void incr(){
		synchronized (lock) {
			cnt++;
		}
	}
}

ReentrantLock

你也可以使用读写锁,当然这里只是很简单的用法。ReentrantLock可以使用在更高级的场景。在这个简单的自增例子中也是很简单粗暴,基本上体现不出它的优势。

private static class Counter{
		
	ReentrantLock lock = new ReentrantLock();
	
	public int cnt = 0;
	
	public void incr(){
		lock.lock();
		try{
			cnt++;
		}
		catch (Exception ex) {
			// TODO: handle exception
		}
		finally {
			lock.unlock();
		}
	}
}

原子类型

JDK 1.5 开始提供了 原子类型,可修改为

private static class Counter{
		
    public AtomicInteger cnt = new AtomicInteger(0);
    
    public void incr(){
    	cnt.incrementAndGet();
    }
}

原子类型很适合使用来做自增,统计等操作。而且与前面二者不同,使用的是无锁算法。

volatile

volatile的语义是被它修饰的变量如果发生变更能立刻被其他线程所见。但是它并不适合自增的场景。

private static class Counter{
	
	public volatile int cnt = 0;
	
	public void incr(){
		cnt++;
	}
}

上面的代码是无法避免问题的,原因是它只能保证可见性,但是自增操作并不是原子性的操作。cnt++是先读取cnt的值然后执行加一并赋值的操作。

volatile的使用原则是可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。如下面shutdown方法并不依赖任何其他状态

volatile  boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do something
    }
}

小结

Java的线程通信模型使用的是共享变量,处理共享变量的同步问题不一定都是需要使用synchronized这种开销比较大的,并发度比较低的方式,可以选的方式还有volatile,lock或者原子类型