Java并发(三)synchronized、volatile以及底层实现

今天说一下并发的基本同步工具synchronized以及volatile。

  • synchronized
    synchronized是并发编程中接触的最基本的同步工具,是一种重量级锁,也是java内置的同步机制,首先我们知道synchronized提供了互斥性的语义和可见性,那么我们可以通过使用它来保证并发的安全。
  • synchronized三种用法:
    1. 对象锁
      当使用synchronized修饰类普通方法时,那么当前加锁的级别就是实例对象,当多个线程并发访问该对象的同步方法、同步代码块时,会进行同步。
    2. 类锁
      当使用synchronized修饰类静态方法时,那么当前加锁的级别就是类,当多个线程并发访问该类(所有实例对象)的同步方法以及同步代码块时,会进行同步。
    3. 同步代码块
      当使用synchronized修饰代码块时,那么当前加锁的级别就是synchronized(X)中配置的x对象实例,当多个线程并发访问该对象的同步方法、同步代码块以及当前的代码块时,会进行同步。
      使用同步代码块时要注意的是不要使用String类型对象,因为String常量池的存在,所以很容易导致出问题
  • synchronized实现原理
    synchronized与其他锁不同,它是内置在JVM中的,从JVM规范中看,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
    方法级的同步是隐式的, 即无须通过字节码指令来控制, 它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时, 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置, 如果设置了, 执行线程就要求先成功持有管程, 然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成) 时释放管程。 在方法执行期间, 执行线程持有了管程, 其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常, 并且在方法内部无法处理此异常, 那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
    我们通过一段代码来看一下:

public class Test {

  static final Test object = new Test();
  static final Test object1 = new Test();
  public static void main(String[] args) throws InterruptedException {
    Test t = new Test();
   new Thread(new Runnable() {
     @Override
     public void run() {
       t.test();
     }
   }).start();

  }

  int a(int a) {
    if (a == 0) {
      throw new NullPointerException();
    } else {
      return a;
    }
  }

  public void test() {

    synchronized (object) {
      //进入同步快,获取锁
      try {
        System.out.println(Thread.currentThread().getId() + "获得锁");
        Thread.sleep(3000);
        //当前同步块结束,释放锁
        System.out.println(Thread.currentThread().getId() + "释放锁");
      } catch (InterruptedException e) {

      }
    }
  }

这里的代码大家可以自己运行一下,这只是让大家便于理解,下面我们来看一下程序实际是如何运行的,我把test()的字节码给大家标注了一下

 // 获取Test类静态实例对象字段,将值压入操作数栈顶
 0 getstatic #11 <Test.object>
 //复制栈顶数值并将复制值压入栈顶
 3 dup
 //将栈顶引用类型数值存入第2个本地变量
 4 astore_1
 //获得对象锁
 5 monitorenter
 6 getstatic #12 <java/lang/System.out>
 9 new #13 <java/lang/StringBuilder>
12 dup
13 invokespecial #14 <java/lang/StringBuilder.<init>>
16 invokestatic #15 <java/lang/Thread.currentThread>
19 invokevirtual #16 <java/lang/Thread.getId>
22 invokevirtual #17 <java/lang/StringBuilder.append>
25 ldc #18 <获得锁>
27 invokevirtual #19 <java/lang/StringBuilder.append>
30 invokevirtual #20 <java/lang/StringBuilder.toString>
33 invokevirtual #21 <java/io/PrintStream.println>
36 ldc2_w #22 <3000>
39 invokestatic #24 <java/lang/Thread.sleep>
42 getstatic #12 <java/lang/System.out>
45 new #13 <java/lang/StringBuilder>
48 dup
49 invokespecial #14 <java/lang/StringBuilder.<init>>
52 invokestatic #15 <java/lang/Thread.currentThread>
55 invokevirtual #16 <java/lang/Thread.getId>
58 invokevirtual #17 <java/lang/StringBuilder.append>
61 ldc #25 <释放锁>
63 invokevirtual #19 <java/lang/StringBuilder.append>
66 invokevirtual #20 <java/lang/StringBuilder.toString>
69 invokevirtual #21 <java/io/PrintStream.println>
72 goto 76 (+4)
75 astore_2
76 aload_1
//释放对象锁
77 monitorexit
//没有异常发生,直接返回
78 goto 86 (+8)
81 astore_3
82 aload_1
//这里是catch块中,当发行异常,无条件释放锁
83 monitorexit
84 aload_3
85 athrow
86 return

可以看到,synchronized在底层是如何运行的。

  • volatile
    • 可见性
      我们知道volatile可以看做是一种synchronized的轻量级锁,他能够保证并发时,被它修饰的共享变量的可见性,那么他是如何实现可见性的呢?
      我们从jmm的角度来看一下,每个线程拥有自己的工作内存,实际上线程所修改的共享变量是从主内存中拷贝的副本,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    • 实现原理
      被volatile修饰的共享变量在进行写操作的时候:
      1. 将当前处理器缓存行的数据写回到系统内存。
      2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
        为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
    • 使用场景
      1.访问变量不需要加锁(加锁的话使用volatile就没必要了)
      1. 对变量的写操作不依赖于当前值(因为他不能保证原子性)
        3.该变量没有包含在具有其他变量的不变式中。
        综上所述:一般我们会用来修饰状态标志;读写锁(读>>写,对写加锁,读不加锁);DCL的单例模式中;volatile bean(例如放入HTTPSession中的对象)
        了解完上面的知识,我们来做一下对比:
  • 相同点:都保证了可见性
  • 不同点 : volatile不能保证原子性,但是synchronized会发生阻塞(在线程状态转换中详说),开销更大。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容