java的并发体系也是非常庞大,而且注意点非常多。这里可能不会面面俱到(否则就是写书了)。
之前写过一篇关于java并发方式的文章。
这里以一个从重到轻的方式来详细聊聊java的并发。
Synchronized和ReentrantLock
最常见的同步方式是synchronized,之所以叫synchronized是因为synchronized会将锁作用于整个对象或者类,而不是具体某个方法,有几种用法,加在方法上,同步代码块以及同步整个class,前两者作用于对象,最后一种方式作用于类。其次是ReentrantLock,理解起来更加容易,就是一个锁,持锁的线程能够进入执行,无锁的线程则阻塞等待.
使用锁容易犯的错误是没有搞清楚需要同步内容的范围。如下例,两个变量都已经作为原子变量来操作,然而,我们真正需要同步的语义是两个状态同时变化,因此实际上没有起到相应的作用。
class SynTest{
private AtomicReference stateA;
private AtomicInteger stateB;
public void syn(boolean b){
if(b){
stateA.set("aaa");
stateB.set(10);
return ;
}
stateA.set("bbb");
stateB.set(5);
}
}
锁优化
减少锁持有时间。基本思想就是最小化锁定持有的代码段,不该锁定的代码不要加锁增加时间成本。
减少锁粒度。实际上是在减小数据结构的锁定部分。比如ConcurrentHashMap中对segment做的分段锁
锁分离。基本思想是对大锁做锁的拆分。比如ReadWriteLock就是从读和写的维度来分离了读写锁;LinkedBlockingQueue中对队头和队尾的put和take的锁分离。
锁粗化。过度减少锁的持有时间有可能会带来频繁的加减锁,典型的例子是循环内加锁。
对于synchronized中锁优化的方式:
最初采用偏向锁,目的是解决同一个线程多次获取锁的开销,当一个线程首次获得锁之后,该锁即进入偏向模式,使得今后同一个线程再次获取该锁时无需额外开销;如果发现线程经常在抢占切换,则偏向锁失效,变为轻量级锁,轻量锁适合线程交替执行,但不是同时抢占的场合,轻量锁采用CAS方式将Mark Word替换为指向锁记录的指针,如果失败,则自旋获取锁;如果自旋获取锁仍然失败,则轻量级锁膨胀为重量级锁。
使用线程安全的数据结构
线程安全的数据结构有很多,如Vector,Hashtable,ConcurrentHashMap, ConcurrentLinkedList等等,关于数据结构,还得另外写文再讲讲。
一个常见的错误是,误以为只要使用了这些数据结构,就一定不会出现线程安全的问题。比如下面的例子,就有可能会出现一个ArrayIndexOutOfBoundsException。
public class VectorTest{
private static Vector v = new Vector();
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循环同步(见注释)。
atomic原子家族
Atomic家族主要使用Unsafe类,见这篇文章。
无锁对象引用主要使用AtomicReference,但是会带来ABA问题,使用AtomicStampedReference可以解决该问题。AtomicStampedReference在原引用比较的基础上增加了stamp的比较,因而可以解决ABA这种过程变化而结果不变的问题。
java的内存模型与volatile对于不同的线程有各自的工作内存,其操作都是先把数据装载进自己的工作内存中,然后同步回主存。这个过程并非原子性的,比如从主存中读内容需要两步read,load,向主存写内容也需要两步store,write。因此还有锁的语义lock, unlock。
java内存模型最主要揭示的一点就是操作的原子性,可见性及有序性。原子性指的就是一件事被当做整体做完。可见性指一个线程的写操作能够立马被另一个线程看到。
而volatile最主要保证的就是内存的可见性和禁止指令重排。它并没有保证原子性,这点要注意,很多时候会以为用了volatile就线程安全了,其实不然,看下面的例子(来源网上)。
public class JoinThread extends Thread
{
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);
}
}
这个程序实际上总是打印出来小于1000的值,这说明对于++运算和n = n + 1这样的运算而言,volatile不能保证其原子性。要保证其原子性,只需要将volatile换为AtomicInteger,再将n++换为n.addAndSet(1)。从字节码来看,++指令实际上被翻译成了这么几个指令,而这几个指令的执行并没有上锁,因此++操作对于上层而言是一个操作,但是底层是多个操作,不具有原子性(《深入理解JVM》一书中更是提到,即便在字节码层面的单操作,其底层汇编有可能是多个操作)。
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;
在一个线程中
doSomeLoading();
a=true;
在另一个线程中
if(a){
doSomeWork();
}
第一个线程先要做一些loading的工作,完成之后将标志设为true,这样第二个线程才能真正做事。但实际上a=true有可能被重排到doSomeLoading之前,这样另一个线程有可能会在没有loading的情况先做doSomeWork导致出错。
关于并发包的内容
一是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
JAVA中的unsafe类 http://xiaobaoqiu.github.io/blog/2014/11/08/jie-mi-sun-dot-misc-dot-unsafe/
synchronized、锁、多线程同步的原理是咋样的 https://www.jianshu.com/p/5dbb07c8d5d5
深入理解Java并发之synchronized实现原理 http://blog.csdn.net/javazejian/article/details/72828483