【原创】Java并发编程系列07 | synchronized原理

【原创】Java并发编程系列07 | synchronized原理

收录于话题
#进阶架构师 | 并发编程专题
12个

点击上方“java进阶架构师”,选择右上角“置顶公众号”
20大进阶架构专题每日送达
【原创】Java并发编程系列07 | synchronized原理
并发编程中用到最多的关键字毫无疑问是synchronized。这篇文章就来探究下synchronized:
synchronized如何使用?
synchronized是实现同步加锁的原理?
synchronized解决了并发编程的哪些问题?
【原创】Java并发编程系列07 | synchronized原理html

1. synchronized使用

1.1 线程安全问题
并发编程中,当多个线程同时访问同一个资源的时候,就会存在线程安全问题。
因为每一个线程执行的过程是不可控的,因此极可能致使最终的结果与实际指望的结果相违背或者直接致使程序出错。
举例:java

public classVolatileTest {
    public int inc = 0;

    public void increase() {
       inc++;
    }

    public static void main(String[] args) {
       final VolatileTest test = newVolatileTest();
       for (int i = 0; i < 10; i++) {
           new Thread() {
              public void run() {
                  for (int j = 0; j < 1000;j++)
                     test.increase();
              };
           }.start();
       }

       while (Thread.activeCount() > 1)
           // 保证前面的线程都执行完
           Thread.yield();
       System.out.println(test.inc);
    }
}

目的:test.inc = 10000
结果:屡次执行获得的结果都小于10000
分析:线程安全问题。
当某个时间test.inc=2,有多个线程同时读取到test.inc=2,而且同时执行加1操做,这些线程的这次操做都执行以后test.inc=3。也就是说执行了多个加1操做,却只将结果增长了1,因此致使最终结果始终小于10000。c++

基本上全部的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称做同步互斥访问。
一般来讲,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其余线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。面试

Java中用synchronized标记同步块。编程

  • 同步块在Java中是同步在某个对象上(监视器对象)。
  • 全部同步在一个对象上的同步块在同一时间只能被一个线程进入并执行操做。
  • 全部其余等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
    1.2 synchronized用法安全

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象
    举例:
public class MyClass{
    int count;

    // 1.实例方法
    public synchronized void add(int value){
        count += value;
    }

    // 2.实例方法中的同步块 (等价于1)
    public void add(int value){
        synchronized(this){
            count += value;
        }
    }

    // 3.静态方法
    public static synchronized void add(intvalue){
         count += value;
    }

    // 4.静态方法中的同步块 (等价于3)
    public static void add(int value){
        synchronized(MyClass.class){
            count += value;
        }
    }
}

2. 原理探究


以下代码,利用javap工具查看生成的class文件信息来分析Synchronize的实现。
代码:markdown

public class synchronized Test {
    // 同步代码块
    public void doSth1(){
       synchronized (synchronizedTest.class){
           System.out.println("HelloWorld");
       }
    }
    // 同步方法
    public synchronized void doSth2(){
        System.out.println("HelloWorld");
    }
}

使用javap对class文件进行反编译后结果:
javap命令:
D:\install\java\jdk8\bin\javap.exe -v .\synchronizedTest.class架构

同步代码块并发

同步方法
从反编译后的结果中能够看到:对于同步方法,JVM采用ACC_synchronized标记符来实现同步。对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。
同步代码块oracle

JVM采用monitorenter、monitorexit两个指令来实现同步。
查询JVM规范The Java® Virtual Machine Specification[1]中关于monitorenter和monitorexit的介绍:
【原创】Java并发编程系列07 | synchronized原理
大体内容以下:
能够把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。
每一个对象维护着一个记录着被锁次数的计数器。
未被锁定的对象的该计数器为0,当一个线程得到锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次得到该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。
当计数器为0的时候。锁将被释放,其余线程即可以得到锁。
同步方法

JVM采用ACC_synchronized标记符来实现同步。
查询JVM规范The Java® Virtual Machine Specification[2]中关于方法级同步的介绍:
【原创】Java并发编程系列07 | synchronized原理
大体内容以下:
方法级的同步是隐式的。同步方法的常量池中会有一个ACC_synchronized标志。
当某个线程要访问某个方法的时候,会检查是否有ACC_synchronized,若是有设置,则须要先得到监视器锁(monitor),而后开始执行方法,方法执行以后再释放监视器锁。这时若是其余线程来请求执行方法,会由于没法得到监视器锁而被阻断住。
值得注意的是,若是在方法执行过程当中,发生了异常,而且方法内部并无处理该异常,那么在异常被抛到方法外面以前监视器锁会被自动释放。

3. Monitor


不管是同步方法仍是同步代码块都是基于监视器Monitor实现的。
Monitor是什么?

全部的Java对象是天生的Monitor,每个Java对象都有成为Monitor的潜质,由于在Java的设计中,每个Java对象自打娘胎里出来就带了一把看不见的锁,它叫作内部锁或者Monitor锁。
每一个对象都存在着一个Monitor与之关联,对象与其Monitor之间的关系有存在多种实现方式,如Monitor能够与对象一块儿建立销毁。
Moniter如何实现线程的同步?

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数

  • 线程T等待对象锁:_EntryList中加入T。
  • 线程T获取对象锁:_EntryList移除T,_owner置为T,计数器_count加1。
  • 线程T中锁对象调用wait():_owner置为null,计数器_count减1,_WaitSet中加入T等待被唤醒。
  • 持有对象锁的线程T执行完毕:复位变量的值,以便其余线程进入获取monitor。
    【原创】Java并发编程系列07 | synchronized原理

    4. 解决三大问题


保证原子性

在并发编程中的原子性:一段代码,或者一个变量的操做,在一个线程没有执行完以前,不能被其余线程执行。
synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放以前,没法被其余线程访问到。

即便在执行过程当中,CPU时间片用完,线程放弃了CPU,但并无进行解锁。而因为synchronized的锁是可重入的,下一个时间片仍是只能被他本身获取到,仍是会由同一个线程继续执行代码,直到全部代码执行完。从而保证synchronized修饰的代码块在同一时间只能被一个线程访问。

保证有序性

若是在本线程内观察,全部操做都是自然有序的。
——《深刻理解Java虚拟机》

单线程重排序要遵照as-if-serial语义,无论怎么重排序,单线程程序的执行结果都不能被改变。由于不会改变执行结果,因此无须关心这种重排的干扰,能够认为单线程程序是按照顺序执行的。
synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。因此,能够保证其有序性。
保证可见性

加锁的含义不只仅局限于互斥行为,还包括可见性。
——《Java并发编程实战》

JMM关于synchronized的两条语义规定保证了可见性:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁前,将清空工做内存中共享变量的值,从而使用共享变量时须要从主内存中从新读取最新的值。

    5. 总结


多并发编程中经过同步互斥访问临界资源来解决线程安全问题,Java中经常使用synchronized标记同步块达到加锁的目的。
synchronized用法有两种,修饰方法和修饰同步代码块。
synchronized的实现原理:每个Java对象都会关联一个Monitor,经过Monitor对线程的操做实现synchronized对象锁。
并发编程中synchronized能够保证原子性、可见性、有序性。
参考资料

[1]
The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
[2]
The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

并发系列文章汇总(点击标题可跳转)


【原创】Java并发编程系列01 | 开篇获奖感言
【原创】Java并发编程系列02 | 并发编程三大核心问题
【原创】Java并发编程系列03 | 重排序-可见性和有序性问题根源
【原创】Java并发编程系列04 | Java内存模型详解
【原创】Java并发编程系列05 | 深刻理解volatile
【原创】Java并发编程系列06 | 你不知道的final
———— e n d ————
微服务、高并发、JVM调优、面试专栏等20大进阶架构师专题请关注公众号【Java进阶架构师】后在菜单栏查看。
回复【架构】领取架构师视频一套。
【原创】Java并发编程系列07 | synchronized原理原创历来不开赞扬是由于我以为你的“在看”,就是给我最好的赞扬^_^

相关文章
相关标签/搜索