°

原 Java多线程中的synchronized、volatile和无锁编程


1、Java线程的状态

1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入 等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁( synchronized)被别的线程占用,则JVM会把该线程放入 锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
锁池:Java多线程中有两种同步锁synchronized和Lock,其中Lock关键字是JDK1.5之后新加入的锁,锁具有排他性,当一个线程获得锁之后,其他线程只能等待其他线程释放该锁,等待的线程也就进入了锁池。
等待池:当线程调用Object.wait()或者Condition.await()时,程序所在的线程会释放其所占有的资源(相应的会释放 synchronized和Lock锁),而进入等待池,等待池当中的线程会等待其他线程调用 Object.notifyAll(),Object.notify()或 者 Condition.signalAll(),Condition.signal()唤醒,这样进入等待的线程就进入等待池,从等待池出来之后进入锁 池,获得锁之后便可进行工作了。
需要说明的是, synchronized锁和调用wait()的对象应为同一对象!否则会报java.lang.IllegalMonitorStateException错误。正确方式如下:
[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. public synchronized static void function04() {//类锁  
  2.        try {  
  3.            Test05.class.wait();//本类的wait池  
  4.        } catch (InterruptedException e) {  
  5.            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.  
  6.        }  
  7.   
  8.    }  
  9.   
  10.    public void function02() {  
  11.        synchronized (lock) {//lock锁  
  12.            try {  
  13.                lock.wait();//同样为lock锁的wait池  
  14.            } catch (InterruptedException e) {  
  15.                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.  
  16.            }  
  17.   
  18.        }  
  19.    }  

Java线程状态的转换图如下:
原                         Java多线程中的synchronized、volatile和无锁编程
其中Thread.join()调用的是Object.wait()方法实现的,意思是让当前线程等待。 是当前调用thread1.join()的线程等待,而不是让thread1等待

2、并发编程的思考

并发安全性的几个相关因素:可见性、顺序性、原子性。关于这三者的详细描述,见 原子性与可见性。其中原子性可以引申为互斥性,而顺序性的产生是原子性的结果即有了原子性才有了顺序性,因此以上三个因素可以推导为 可见性和互斥性。根据并发安全的特性,对synchronized关键字、volatile关键字和无锁编程(Unsafe)三种并发处理的效果如下:

 

可见性

互斥性

synchronized

块可见

块互斥

volatile

变量可见

变量互斥(无意义)

无锁编程(Unsafe)

变量可见

不保证

3、synchronized关键字

synchronized关键字一般情况下有以下几种用法:
[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. /** 
  2.  * Created with IntelliJ IDEA. 
  3.  * User: yangzl2008 
  4.  * Date: 14-10-25 
  5.  * Time: 下午8:31 
  6.  * To change this template use File | Settings | File Templates. 
  7.  */  
  8. public class TeshSynchronized {  
  9.     Object lock = new Object();  
  10.     public synchronized void function01() {  
  11.     }  
  12.     public void function02() {  
  13.         synchronized (lock) {  
  14.         }  
  15.     }  
  16.     public void function03() {  
  17.         synchronized (this) {  
  18.         }  
  19.     }  
  20.     public synchronized static void function04() {  
  21.     }  
  22.     public void function05() {  
  23.         synchronized (TeshSynchronized.class) {  
  24.         }  
  25.     }  
  26. }  

以上synchronized关键字的用法可以根据锁的不同分为两类, 对象锁类锁

对象锁,其中function01()、function02()、function03()用的是实例锁的形式,这种对象锁只对同一实例上不 同线程有互斥作用。在多线程环境当中,调用同一对象的function01()、function02()、function03()是互斥的。
类锁,如function04()、function05(),这种锁对于同一类的不同线程都具有互斥作用。在多线程环境当中,调用不同对象的function04()、function05()是互斥的。

 

同一对象

不同对象但同一类

对象锁

多线程互斥

多线程不互斥

类锁

多线程互斥

多线程互斥

synchronized保证的是synchronized块级别的互斥性和可见性。
块级别的互斥性:当有一个线程获得synchronized的锁之后,其他线程不能进入这个块,而只能等获得锁的线程执行完毕之后,在进入这个块。
块级别的可见性:在多线程环境下,当一个线程进入synchronized块后,其修改的变量值在其他线程当中能够看到这个值。
基于以上以上两个特性,synchronized关键字能够保证多线程安全,这是真正意义上线程安全。

4、volatile关键字

volatile关键字,根据清英文章 聊聊并发(一)深入分析Volatile的实现原理,可知道当我们在一个变量之上volatile之后在多核处理器下会引发了两件事情。
  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
volatile在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. /** 
  2.  * Created with IntelliJ IDEA. 
  3.  * User: yangzl2008 
  4.  * Date: 14-10-26 
  5.  * Time: 下午10:09 
  6.  * To change this template use File | Settings | File Templates. 
  7.  */  
  8. public class TestVolatile {  
  9.     private volatile int a1;  //多线程可见  
  10.     private int a2;   //多线程有问题  
  11.     private int a3;  
  12.   
  13.     public int getA1() {  
  14.         return a1;  
  15.     }  
  16.   
  17.     public void setA1(int a1) {  
  18.         this.a1 = a1;  
  19.     }  
  20.   
  21.     public int getA2() {  
  22.         return a2;  
  23.     }  
  24.   
  25.     public void setA2(int a2) {  
  26.         this.a2 = a2;  
  27.     }  
  28.   
  29.     public int getA3() {  
  30.         return a3;  
  31.     }  
  32.   
  33.     public synchronized void setA3(int a3) {  
  34.         this.a3 = a3;  
  35.     }  
  36.   
  37. }  

以上代码,我们来看看volatile的变量可见性。
对于a2,当线程调用setA2()方法对a2设值时,因为每个线程都有缓存,因此此时有可能会造成其他线程看不到新的值,而需要等到a2的同步到内存当中后,其他线程读内存时才能看到,存在多线程问题。
对于a1,因为volatile保证了a1只有一份数据在内存当中,因此,其他线程是可见的。
对于a3,因为其set方法使用synchronized 关键字,synchronized 关键字能够保证块可见性,因此其他线程是可见的。
由以上分析可知,volatile实现了synchronized 一样的多线程安全的效果。但是其实现的仅仅是可见性,对于块互斥性,并没有实现。看一下例子:
[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. /** 
  2.  * Created with IntelliJ IDEA. 
  3.  * User: yangzl2008 
  4.  * Date: 14-10-26 
  5.  * Time: 下午10:21 
  6.  * To change this template use File | Settings | File Templates. 
  7.  */  
  8. public class TestVolatile2 {  
  9.   
  10.     volatile int count;  
  11.     Map<String, String> map = new ConcurrentHashMap<String, String>();  
  12.   
  13.     public void addContent(String key, String value) {  
  14.         if (count < 100) {  
  15.             map.put(key, value);  
  16.             count++;  
  17.         }  
  18.     }  
  19.   
  20.     @Test  
  21.     public void testAddContent() throws Exception {  
  22.         ExecutorService executorService = Executors.newFixedThreadPool(10);  
  23.   
  24.         for (int i = 0; i < 10; i++) {  
  25.             executorService.execute(new AddContentTask());  
  26.         }  
  27.         // 关闭启动线程  
  28.         executorService.shutdown();  
  29.         // 等待子线程结束,再继续执行下面的代码  
  30.         executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);  
  31.   
  32.         System.out.println(map.size());  
  33.   
  34.     }  
  35.   
  36.     private final class AddContentTask implements Runnable {  
  37.   
  38.         @Override  
  39.         public void run() {  
  40.             //每个线程放11次  
  41.             for (int i = 0; i <= 10; i++) {  
  42.                 addContent(Thread.currentThread().getName() + ” “ + System.currentTimeMillis() + ” “ + i, “value”);  
  43.             }  
  44.         }  
  45.     }  
  46.   
  47.   
  48. }  

以上判断count判断到达100后,就无法再向map当中放东西,但实际上,map当中的数量绝大多数情况下是大于100的。因此,volatile只能保证变量的可见性,而并不能保证块的互斥性,在某些情况下,其是无法代替synchronized的。

5、无锁编程

Java当中的无锁编程通过sun.misc.Unsafe实现的。我们以AtomicInteger源码来分析一下,其在多线程下的运作方式。首先,Unsafe通过内存偏移量得到要变量的内存位置,代码如下:
[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. static {  
  2.      try {  
  3.        valueOffset = unsafe.objectFieldOffset  
  4.            (AtomicInteger.class.getDeclaredField(“value”));  
  5.      } catch (Exception ex) { throw new Error(ex); }  
  6.    }  
  7.   
  8.    private volatile int value;  

在我们调用getAndIncrement时,其代码如下:

[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. public final int getAndIncrement() {  
  2.        for (;;) {  
  3.            int current = get();  
  4.            int next = current + 1;  
  5.            if (compareAndSet(current, next))  
  6.                return current;  
  7.        }  
  8.    }  

compareAndSet的代码如下:

[java] view plain copy 原                         Java多线程中的synchronized、volatile和无锁编程 原                         Java多线程中的synchronized、volatile和无锁编程

  1. public final boolean compareAndSet(int expect, int update) {  
  2.     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
  3.     }  

其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);是一个本地方法。

CAS (compare and swap)操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
在认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。他是非阻塞的。
从某种意义上来看,简单的复合操作,不管是getAndInc和getAndDec还有IncAndGet、DecAndGet等等,其实都可以归结为一 个CAS操作,比如getAndIncrement,在for循环内取原值,并且+1,并且和原值比较设置结果,如果成功的话返回,否则继续。
而以上之所以会产生不成功的情况,是因为在多线程情况下,有可能有别的线程已经修改value的值,在比较的时候,value的值跟原先的值不 同,因此其继续进行比较,只有在没有线程改变之后,才能修改value的值。比如,线程A打算修改value的值,但是B线程在这个时候修改了value 的值,A看到value的值变量,继续下一个循环,这时,C线程又来修改了value的值,A看到后只能又进行下一个循环。因此无锁编程,无法保证顺序 性,即无法保证互斥性,因为每个线程都有可能修改value的值,但是value值得修改对每个线程的修改都是可见的。

6、总结

Java多线程中的synchronized、volatile和无锁编程在不同的应用场景下,都能保证线程安全,我们在选择不同的工具时,需 要根据不同的场景选择不同的工具,当然synchronized是肯定能够实现多线程安全的,但是在某些情况下,后两者的效率可能更高,这就需要我们对不 同的业务场景进行仔细的分析,找到最合适的工具!

7、参考

打赏
  喜欢