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

前言

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

synchronized

Java 在 Java 5 以前通过synchronized关键词用来实现锁,由于其由JVM指令隐式实现也被称为隐式锁。不过由于其实现过于底层,所以对性能的影响较大,但随着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("method1() execute!");

    }

    public /*synchronized*/ void method2() {
        System.out.println("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(监视器锁)来实现同步(synchronized关键字),其中同步代码块采用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"

通过查看字节码的反汇编结果,果然如此,下面是JVM规范对monitorentermonitorexit的描述,我做了一个简单的翻译,你可以点击标题查看原文。

monitorenter

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

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

monitorexit

只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令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的实现原理

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

查看该部分源码

参考


查看《浅析Java并发编程》系列文章目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容