java的并发体系也是非常庞大,而且注意点非常多。这里可能不会面面俱到(否则就是写书了)。
之前写过一篇关于java并发方式的文章。
这里以一个从重到轻的方式来详细聊聊java的并发。
Synchronized和ReentrantLock
最常见的同步方式是synchronized,之所以叫synchronized是因为synchronized会将锁作用于整个对象或者类,而不是具体某个方法,有几种用法,加在方法上,同步代码块以及同步整个class,前两者作用于对象,最后一种方式作用于类。其次是ReentrantLock,理解起来更加容易,就是一个锁,持锁的线程能够进入执行,无锁的线程则阻塞等待.
使用锁容易犯的错误是没有搞清楚需要同步内容的范围。如下例,两个变量都已经作为原子变量来操作,然而,我们真正需要同步的语义是两个状态同时变化,因此实际上没有起到相应的作用。
class SynTest{
private AtomicReference<String> stateA;
private AtomicInteger stateB;
public void syn(boolean b){
if(b){
stateA.set("aaa");
stateB.set(10);
return ;
}
stateA.set("bbb");
stateB.set(5);
}
}
使用线程安全的数据结构
线程安全的数据结构有很多,如Vector,Hashtable,ConcurrentHashMap, ConcurrentLinkedList等等,关于数据结构,还得另外写文再讲讲。
一个常见的错误是,误以为只要使用了这些数据结构,就一定不会出现线程安全的问题。比如下面的例子,就有可能会出现一个ArrayIndexOutOfBoundsException。
public class VectorTest{
private static Vector<Integer> v = new Vector<Integer>();
public static void main(String [] args){
while(true){
for(int i = 0; i < 10; i++){
v.add(i);
}
Thread t = new Thread(new Runnable(){
public void run() {
//synchronized(v){
for(int i = 0; i < v.size(); i++)
v.remove(i);
//}
}
});
Thread p = new Thread(new Runnable(){
public void run() {
//synchronized(v){
for(int i = 0; i < v.size(); i++)
System.out.print(v.get(i) + "\t");
//}
}
});
t.start();
p.start();
//while(Thread.activeCount() > 20);
}
}
}
原因是什么呢?Vector内部使用数组保存对象,它对这个数据结构本身的操作如add, remove, get等做了同步,因此多个线程同时操作vector时,能保证vector内部操作数组的过程是安全的。但是如果仔细观察,会发现实际上这里的语义比较微妙,这里的v.size跟remove和get并没有原子性,举个例子,线程p首先调用v.size,得到10,此时线程t抢占处理器并调用了remove删除了所有元素,此时线程p抢占回来准备get(0),于是抛出错误ArrayIndexOutOfBoundsException。v.size和v.get语义上要求原子但实际上没有,这就是问题所在。因此,应该使用一个更大范围的封闭,在v上对整个for循环同步(见注释)。
java的内存模型与volatilevolatile是java中比较难理解的语义之一。要理解volatile,先理解java的内存模型。这里有一些深入讲解内存模型与volatile的文章,推荐 http://www.ibm.com/developerworks/cn/java/j-jtp06197.html, http://ifeve.com/java-memory-model-4/
对于不同的线程有各自的工作内存,其操作都是先把数据装载进自己的工作内存中,然后同步回主存。这个过程并非原子性的,比如从主存中读内容需要两步read,load,向主存写内容也需要两步store,write。因此还有锁的语义lock, unlock。
java内存模型最主要揭示的一点就是操作的原子性跟可见性。原子性指的就是一件事被当做整体做完。可见性指一个线程的写操作能够立马被另一个线程看到。
而volatile最主要保证的就是内存的可见性和禁止指令重排。它并没有保证原子性,这点要注意,很多时候会以为用了volatile就线程安全了,其实不然,看下面的例子(来源网上)。
public class JoinThread extends Thread这个程序实际上总是打印出来小于1000的值,这说明对于++运算和n = n + 1这样的运算而言,volatile不能保证其原子性。要保证其原子性,只需要将volatile换为AtomicInteger,再将n++换为n.addAndSet(1)。从字节码来看,++指令实际上被翻译成了这么几个指令,而这几个指令的执行并没有上锁,因此++操作对于上层而言是一个操作,但是底层是多个操作,不具有原子性(《深入理解JVM》一书中更是提到,即便在字节码层面的单操作,其底层汇编有可能是多个操作)。
{
public static volatile int n = 0;
public void run()
{
for (int i = 0; i < 10; i++)
try
{
n++;
sleep(3); // 为了使运行结果更随机,延迟3毫秒
}
catch (Exception e)
{
}
}
public static void main(String[] args) throws Exception
{
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++)
// 建立100个线程
threads[i] = new JoinThread();
for (int i = 0; i < threads.length; i++)
// 运行刚才建立的100个线程
threads[i].start();
for (int i = 0; i < threads.length; i++)
// 100个线程都执行完后继续
threads[i].join();
System.out.println("n=" + JoinThread.n);
}
}
5 getstatic org.metro.core.JoinThread.n : int [10]对于 指令重排,也是防不胜防,比如如下语句
8 iconst_1
9 iadd
10 putstatic org.metro.core.JoinThread.n : int [10]
boolean a=false;第一个线程先要做一些loading的工作,完成之后将标志设为true,这样第二个线程才能真正做事。但实际上a=true有可能被重排到doSomeLoading之前,这样另一个线程有可能会在没有loading的情况先做doSomeWork导致出错。
在一个线程中
doSomeLoading();
a=true;
在另一个线程中
if(a){
doSomeWork();
}
个人愚见,除非你很清楚在做什么,否则尽量使用Atomic变量代替volatile,因为其性能差异并不大。有意思的是,AtomicReference又在内部实现中用volatile来修饰被引用的对象,当然是希望用到volatile的可见性。
关于并发包的内容
一是ThreadPool;二是Callable与Future(关于future的其中一种用法,参考java超时控制);三是一些同步工具类,如CountDownLatch,Semaphore, Barrier
AQS http://blog.csdn.net/vernonzheng/article/details/8275624
深入锁机制 http://blog.csdn.net/chen77716/article/details/6641477
JUC并发类详解 http://my.oschina.net/foxeye/blog/625886