浅析 Java 并发编程(二)synchronized & volatile

《浅析 Java 并发编程(二)synchronized & volatile》

前言

Java 自首个版本便提供了多线程的支持,并为开发者提供了synchronized、volatile关键字用于解决并发下线程数据同步的问题。在Java 5 以前开发者也只能使用这两个关键词解决同步问题,比较“简单粗暴”缺乏灵活性。在Java 5 java.util.concurrent包诞生后才有了更多的选择,在后续文章会介绍。本文是作者自己对synchronized、volatile关键字的理解与总结,不对之处,望指出,共勉。

synchronized

synchronized关键词也被称之为锁,更确切的说法应该是Java 通过synchronized实现了锁。由于其实现较“重”,也被称为重量级锁,对性能的影响较大,随着Java 6对其进行了一些优化后,有了一定改善。synchronized是解决本系列第一篇文章说到的互斥性、原子性、可见性、有序性问题的最直接、简单的方式。

使用

synchronized关键字可以用在代码块和方法上,表示在执行整个代码块或方法之前线程必须取得合适的锁(monitor)。被synchronized修饰的代码块或方法,每次只允许一个线程进入(获得锁)执行,如果其他线程试图进入(不管是同一同步块还是不同的同步块),JVM会将它们挂起(放入到等锁池中)。这种结构在并发理论中称为临界区(critical section)。

  • 修饰成员方法,线程要取得类的当前实例对象的锁方可执行
public class SynchronizedMethodTest {

    public /*synchronized*/ void method1() {
        try {
            //模拟方法需要执行100毫秒
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this + " method1() execute!");

    }

    public /*synchronized*/ void method2() {
        System.out.println(this + " method2() execute!");
    }

    public static void main(String[] args) {
        final SynchronizedMethodTest test = new SynchronizedMethodTest();
        // 使用Java 8 lambda 简化代码
        new Thread(() -> test.method1()).start();
        new Thread(() -> test.method2()).start();

        /**
         输出:
             method2() execute!
             method1() execute!

         使用synchronized修饰方法后:
             method1() execute!
             method2() execute!
         **/
    }
}
  • 修饰静态方法,线程要取得类的Class对象的锁方可执行
public class SynchronizedStaticMethodTest {

    public /*synchronized*/  static void method1() {
        try {
            //模拟方法需要执行100毫秒
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method1() execute!");

    }

    public /*synchronized*/ static void method2() {
        System.out.println("method2() execute!");
    }

    public static void main(String[] args) {
        final SynchronizedStaticMethodTest test1 = new SynchronizedStaticMethodTest();
        final SynchronizedStaticMethodTest test2 = new SynchronizedStaticMethodTest();
        // 使用Java 8 lambda 简化代码
        new Thread(() -> test1.method1()).start();
        new Thread(() -> test2.method2()).start();

        new Thread(() -> SynchronizedStaticMethodTest.method1()).start();
        new Thread(() -> SynchronizedStaticMethodTest.method2()).start();

        /**
         输出:
         method2() execute!
         method2() execute!
         method1() execute!
         method1() execute!

         使用synchronized修饰方法后:
         method1() execute!
         method2() execute!
         method1() execute!
         method2() execute!
         **/
    }
}
  • 修饰代码块,程序员可以指定要取得的是哪个对象(包括Class对象)的锁,线程需获得该锁方可执行
public class SynchronizedCodeBlockTest {
    private final Object lock = new Object();

    public void method1() {
        //需获得Class对象的锁方可执行
        //synchronized (this.getClass())
        //需获得lock对象的锁方可执行
        //synchronized (lock)
        //需获得当前对象的锁方可执行
        synchronized (this) {
            try {
                //模拟方法需要执行100毫秒
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("method1() execute!");
        }
    }

    public void method2() {
        //需获得Class对象的锁方可执行
        //synchronized (this.getClass())
        //需获得lock对象的锁方可执行
        //synchronized (lock)
        //需获得当前对象的锁方可执行
        synchronized (this) {
            System.out.println("method2() execute!");
        }
    }

    public static void main(String[] args) {
        final SynchronizedCodeBlockTest test = new SynchronizedCodeBlockTest();
        // 使用Java 8 lambda 简化代码
        new Thread(() -> test.method1()).start();
        new Thread(() -> test.method2()).start();
        /**
         输出:
             method1() execute!
             method2() execute!

         */
    }
}
实现原理

Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).

For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10).

上面这段话来自The Java® Virtual Machine Specification 3.14. Synchronization,简单来说就是JVM使用monitor(监视器锁)来实现同步,其中同步代码块采用monitorentermonitorexit指令显式的实现,而同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。下面通过javap反汇编指令查看一段简单的代码,看看是否如此。

public class SynchronizedTest {

    public synchronized void method1(){
        System.out.println("Hello World!");
    }

    public  void method2(){
        synchronized (this){
            System.out.println("Hello World!");
        }
    }
}
$ javap -v concurrent/target/classes/sync/SynchronizedTest.class
Classfile /E:/IdeaWorkSpace/java-codes/concurrent/target/classes/sync/SynchronizedTest.class
  Last modified 2017-6-14; size 702 bytes
  MD5 checksum 48cb43f462459cc1eed4ba2e7c1d204a
  Compiled from "SynchronizedTest.java"
public class sync.SynchronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   ...略
{
   ...略
  public synchronized void method1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED//同步方法的实现
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lsync/SynchronizedTest;

  public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter//同步代码块的实现
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello World!
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit//同步代码块的实现
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
       ...
      LineNumberTable:
       ...略
      LocalVariableTable:
       ...略
      StackMapTable: number_of_entries = 2
       ...略
}
SourceFile: "SynchronizedTest.java"

通过查看字节码的反汇编结果,果然如此,下面是The Java® Virtual Machine Specificationmonitorentermonitorexit的描述,我做了一个简单的翻译,你可以点击标题查看原文。

monitorenter

每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:

  • 如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
  • 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。
  • 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。

monitorexit

只有拥有相应对象的monitor的线程才能执行monitorenter指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程退出monitor,此时其他阻塞的线程将可以尝试获取该monitor。

总结
  • 只能锁定对象,不能锁定基本数据类型
  • 被锁定的对象数组中的单个对象不会被锁定
  • 同步方法可以视为包含整个方法的synchronized(this) { … }代码块
  • 静态同步方法会锁定它的Class对象
  • 内部类的同步是独立于外部类的
  • synchronized修饰符并不是方法签名的组成部分,所以不能出现在接口的方法声明中
  • 非同步的方法不关心锁的状态,它们在同步方法运行时仍然可以得以运行
  • 代码块同步使用monitorentermonitorexit指令实现
  • 方法同步使用ACC_SYNCHRONIZED标记符实现
  • synchronized实现的锁是可重入的锁

查看该部分源码

volatile

volatile 关键字相对于synchronized是一种简单的同步机制,常用于解决可见性问题(可见性指的是当一个线程对共享变量进行更改时,其他线程对更改后的变量是可见的),因为被volatile修饰的变量遵循以下规则:

  • 变量的值在使用之前总会从主内存中再读取出来。
  • 对变量值的修改总会在完成之后写回到主内存中。

使用volatile关键字可以在多线程环境下预防编译器不正确的优化假设(编译器可能会将在一个线程中值不会发生改变的变量优化成常量),但只有修改时不依赖当前状态(读取时的值)的变量才应该声明为volatile变量。

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

//上面伪代码中 while(!stop) 有可能会被编译器优化为 while(true),进而不能被其他线程中断,导致死循环。
使用

解决可见性问题

public class VolatileTest {

    private /*volatile*/ int sharedValue = 0;

    public static void main(String[] args) throws InterruptedException {
        VolatileTest test = new VolatileTest();
        new Thread(() -> test.listener()).start();
        new Thread(() -> test.increment()).start();

        /**
         输出:
             Value Incrementing:1
             Value Incrementing:2
             Value Incrementing:3
             Value Incrementing:4
             Value Incrementing:5
         使用 volatile 修饰 sharedValue后:
             Value Incrementing:1
             Value Changed:1
             Value Incrementing:2
             Value Changed:2
             Value Incrementing:3
             Value Changed:3
             Value Incrementing:4
             Value Changed:4
             Value Incrementing:5
             Value Changed:5
         */
    }

    public void listener() {
        int localValue = sharedValue;
        while (sharedValue < 5) {
            if (localValue != sharedValue) {
                System.out.println("Value Changed:" + sharedValue);
                localValue = sharedValue;
            }
        }
    }

    public void increment() {
        while (sharedValue < 5) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ++sharedValue;
            System.out.println("Value Incrementing:" + sharedValue);

        }
    }
}

实现双重检查(Double-Checked)

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {
        new AssertionError("don't support reflect.");
    }

    public static Singleton getInstance() {
        if (instance == null) { // Single Checked
            synchronized (Singleton.class) {
                if (instance == null) { // Double checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
实现原理

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” — 《深入理解Java虚拟机》  

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

推荐阅读:深入分析Volatile的实现原理

总结
  • 可用于解决可见性问题
  • 可禁止编译器和处理器对指令进行重排序,能在一定程度上解决有序性问题
  • 不能解决原子性问题

查看该部分源码

参考

    原文作者:JVM
    原文地址: https://juejin.im/entry/59424ba68d6d810058c50fc9
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞