目录

一、概述

二、并发编程中的三个问题

三、Java内存模型(JMM)

四、synchronized保证三大特性

五、synchronized的特性

六、总结


一、概述

大家都知道,synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致共享数据不安全,而synchronized关键字就是用于代码同步。举个例子,12306的票不能超卖、商品库存不能超卖等问题,就可以使用synchronized来保证线程安全。同时并发编程也是目前工作中一个比较棘手的问题,也是面试中经常被问到的一个问题。

synchronized锁的3种使用形式:

  • 修饰普通同步方法:锁的对象是当前实例对象;
  • 修饰静态同步方法:锁的对象是当前的类的Class字节码对象;
  • 修饰同步代码块:锁的对象是Synchronized后面括号里配置的对象,可以是某个对象,也可以是某个类的.class对象;

synchronized的使用可能大部分小伙伴都会,但是对于synchronized的原理可能就了解的不多,因为synchronized是Java中的一个关键字,我们在Java代码中并不能看到synchronized相关的原理。本篇文章将会从多个维度详细分析synchronized关键字的底层原理。

二、并发编程中的三个问题

  • 可见性

可见性是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。

我们通过一个案例来说明一下什么是可见性问题?

一个线程根据boolean类型的标记flflag, while循环,另一个线程改变这个flflag变量的值,另一个线程并不会停止循环。

/*** 一个线程对共享变量的修改,另一个线程不能立即得到最新值*/
public class VisibilityDemo {//共享数据private static boolean flag = true;public static void main(String[] args) throws InterruptedException {//创建一个线程不断读取flagThread t1 = new Thread(() -> {while (flag) {}}, "t1");t1.start();Thread.sleep(2000);//创建一个线程修改共享变量Thread t2 = new Thread(() -> {flag = false;System.out.println("线程t2修改了共享变量flag的值为false......");}, "t2");t2.start();}
}

启动程序,观察控制台,我们发现当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值,所以我们的程序一直卡在那里,如下图:

  • 原子性

原子性指的是在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

同样通过一个示例来说明什么是原子性问题。

定义一个共享变量num,然后启动多个线程,同时执行1000次num++;

import java.util.ArrayList;public class AtomicityDemo {//定义一个共享变量private static int num = 0;public static void main(String[] args) throws InterruptedException {ArrayList<Thread> threads = new ArrayList<>();//启动五个线程同时进行num++for (int i = 0; i < 5; i++) {Thread t = new Thread(() -> {for (int j = 0; j < 1000; j++) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}num++;}});t.start();threads.add(t);}//使用join()等待所有线程都执行完成for (Thread t : threads) {t.join();}System.out.println("num = " + num);}
}

启动程序,观察运行结果:

我们看到,正常输出应该是5000才对,这里输出4676,少了很多。这其中的原因其实就是我们的num++操作并不是原子性的,所以导致多个线程同时对num自增时,结果就可能会出现少加的情况。

下面我们通过反编译的方式来看下num++在JVM字节码里面到底是怎么执行的:使用【 javap -p -v -c class名 >想要保存到的文件名】反汇编class文件,得到下面的字节码指令:

javap -p -v -c AtomicityDemo.class >AtomocityDemo.txt

如下图可以看到,num++在底层分为了三个步骤执行:

如下四条指令之间并不保证原子性,在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如可能会出现多个线程都执行了iadd,但是最后面实际上只加了1,导致结果不正确。

23: getstatic     #18                 // Field num:I
26: iconst_1
27: iadd
28: putstatic     #18                 // Field num:I

所以在并发编程时,我们可能会遇到原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

  • 有序性

有序性是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

这里通过DCL【Double Check双重检查锁】单例模式来说明什么是有序性问题。

public class Singleton {//为了解决有序性,可以加上volatile关键字修饰,禁止指令重排序private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (null == instance) {synchronized (Singleton.class) {if (null == instance) {instance = new Singleton();}}}return instance;}
}
  • instance = new Singleton();

上述实例化对象的语句,在JVM具体执行的时候,实际上分为三个步骤:

  • 1、分配对象的内存空间;
  • 2、初始化对象;
  • 3、设置实例对象指向刚分配的内存地址;

步骤2【初始化对象】需要依赖于步骤1【分配对象的内存空间】,但是步骤3【设置实例对象指向刚分配的内存地址】不需要依赖步骤2【初始化对象】,所以JVM具体执行的时候会对字节码进行优化,有可能出现1 > 3 > 2的执行顺序,当出现这种顺序的时候,虽然instance不为空,但是此时对象有可能没有正确初始化,直接拿来使用的话可能会报错。

也就是说程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是按照我们编写代码时的顺序。

三、Java内存模型(JMM)

Java内存模型,Java Memory Molel,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

  • 主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

  • 工作内存

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

如下图:

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

对应如下的流程图:

注意:

  • 1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值;
  • 2. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中;

主内存与工作内存之间的数据交互过程大概如下:

lock -> read -> load -> use -> assign -> store -> write -> unlock

四、synchronized保证三大特性

synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

synchronized (锁对象) {
// 受保护资源;
}

【a】synchronized与原子性

我们使用前面的num++那个例子,看看如何使用synchronized怎么保证原子性。

public class AtomicityDemo {//定义一个共享变量private static int num = 0;public static void main(String[] args) throws InterruptedException {ArrayList<Thread> threads = new ArrayList<>();//启动五个线程同时进行num++for (int i = 0; i < 5; i++) {Thread t = new Thread(() -> {for (int j = 0; j < 1000; j++) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}synchronized (AtomicityDemo.class) {num++;}}});t.start();threads.add(t);}//使用join()等待所有线程都执行完成for (Thread t : threads) {t.join();}System.out.println("num = " + num);}
}

运行程序,结果如下:

我们看到,加了synchronized同步锁之后,运行结果就正确了。如下,使用synchronized锁住num++这一行代码,保证原子性。

synchronized (AtomicityDemo.class) {//对num++;增加同步代码块后,保证同一时间只有一个线程操作num++,就不会出现安全问题num++;
}

下面我们来看看synchronized保证原子性的原理。使用javap反编译一下AtomicityDemo.class:

可以看到,在num++的字节码指令前后,多出了monitorenter和monitorexit两条指令,这其实就是synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

【b】synchronized与可见性

同样,还是拿之前可见性的代码,看下如何使用synchronized来保证线程可见性。

  • 第一种解决方案:使用volatile关键字保证可见性
private static volatile boolean flag = true;
  • 第二种解决方案:使用synchronized关键字保证可见性
public class VisibilityDemo {//共享数据private static boolean flag = true;//锁对象private static Object obj = new Object();private static Logger logger = LoggerFactory.getLogger(VisibilityDemo.class);public static void main(String[] args) throws InterruptedException {//创建一个线程不断读取flagThread t1 = new Thread(() -> {while (flag) {synchronized (obj) {logger.info("t1线程获取到flag = " + flag);}}}, "t1");t1.start();Thread.sleep(2000);//创建一个线程修改共享变量Thread t2 = new Thread(() -> {flag = false;System.out.println("线程t2修改了共享变量flag的值为false......");}, "t2");t2.start();}
}

运行程序,观察控制台输出:

如上图,我们看到,当线程t1感知到了线程t2修改了主内存的flag变量,所以程序正常终止。synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值,重新从主内存中获取变量最新的值。

【c】synchronized与有序性

int a = 0;
//monitorenter
synchronize (this){ // Load屏障// Acquire屏障a = 10;    //注意:内部还是会发生指令重排int b = a;// Release屏障
}
//monitorexit
//Store屏障

看上述代码,在 monitorenter 指令和 Load 屏障之后,会加一个 Acquire屏障,这个屏障的作用是禁止读操作和读写操作之间发生指令重排,在 monitorexit 指令前加一个Release屏障,也是禁止写操作和读写操作之间发生重排序。这里涉及到内存屏障相关的知识,后面专门再通过一篇文章介绍内存屏障。这里先记住:synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性。

五、synchronized的特性

synchronized主要有两个特性:可重入特性、不可中断特性。

【a】可重入特性

先来了解一下什么是可重入?

可重入指的就是一个线程可以多次执行synchronized,重复获取同一把锁。

我们来看一个可重入锁的案例:

public class RenentrantDemo {//锁对象private static Object obj = new Object();public static void main(String[] args) {//自定义Runnable对象Runnable runnable = () -> {//使用嵌套的同步代码块synchronized (obj) {System.out.println(Thread.currentThread().getName() + "第一次获取锁资源...");synchronized (obj) {System.out.println(Thread.currentThread().getName() + "第二次获取锁资源...");}}};//启动两个线程new Thread(runnable, "t1").start();new Thread(runnable, "t2").start();}
}

启动程序,观察后台输出:

synchronized能够实现可重入的原理,其实就是synchronized的锁对象中有一个计数器会记录线程获得几次锁,并且会记录当前持有锁的线程ID,如果线程再一次加锁,会判断当前线程ID是否跟锁对象里面的线程ID相同, 如果相同则允许重入。

【b】不可中断特性

先解释一下什么是不可中断?

不可中断,指的是一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

通过一个案例来说明synchronized不可中断特性:

public class UninterruptibleDemo {//锁对象private static Object obj = new Object();public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {synchronized (obj) {try {System.out.println(Thread.currentThread().getName()+ " start lock...");Thread.sleep(20000);System.out.println(Thread.currentThread().getName()+ " end lock...");} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+ " InterruptedException...");e.printStackTrace();}}}, "t1");Thread thread2 = new Thread(() -> {synchronized (obj) {try {System.out.println(Thread.currentThread().getName()+ " start lock...");Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+ " end lock...");} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+ " InterruptedException lock...");e.printStackTrace();}}},"t2");thread1.start();thread2.start();Thread.sleep(1000);// 中断t2线程的执行thread2.interrupt();System.out.println("t2 interrupt...");}
}

运行程序,观察输出日志:

由此也验证了synchronized不可中断的特性,只有获取到锁之后才能中断,等待锁时不可中断。

六、总结

本篇文章是Synchronized原理剖析的第一部分,主要总结了并发编程中几个主要特性:原子性、有序性、可见性问题,并通过案例说明了synchronized与这些特性的关系,此外还介绍了JMM内存模型是如何保证可见性等等,由于笔者水平有限,文中如有不对之处,还望大家指正,希望对大家有用。下一篇继续总结synchronized原理剖析的第二部分,将从字节码指令层面去看下synchronized的工作原理。

synchronized工作原理剖析(一)相关推荐

  1. NameNode与DataNode的工作原理剖析

    NameNode与DataNode的工作原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.HDFS写数据流程 1>.客户端通过Distributed FileSys ...

  2. namenode和datanode工作机制_NameNode与DataNode的工作原理剖析

    NameNode与DataNode的工作原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.HDFS写数据流程 1>.客户端通过Distributed FileSys ...

  3. android 充电模式deamon_Android Lint工作原理剖析

    Android Lint是Android SDK提供的一项静态代码分析工具,对于提高代码质量具有重要作用.到目前为止,Android SDK自带的Lint检查项目达到了253项,我们在开发过程中经常见 ...

  4. PGP(Pretty Good Privacy)工作原理剖析

    一.加密和解密(encryption & decryption) 二.传统密码学--加密解密使用同一个key 故key的安全性不言而喻,脑中闪现如下的画面: a person with a l ...

  5. 嵌入式Linux之我行——ARM MMU工作原理剖析

    一.MMU的产生 许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程 ...

  6. ARM MMU工作原理剖析[转]

    一.MMU的产生       许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以 ...

  7. ARM MMU工作原理剖析

    一.MMU的产生 许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程 ...

  8. 服务器是怎么工作的?(一)——DHCP工作原理剖析

    目录 1.什么是DHCP(Dynamic Host Configuration Protocol)

  9. 网关,路由,局域网内的通信及不同的网络间通信实现的原理剖析

    百度百科定义网关: 网关(Gateway)又称网间连接器.协议转换器.网关在网络层以上实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连.网关既可以用于广域网互连,也可以用于局域网 ...

  10. Spark源码解读之Shuffle原理剖析与源码分析

    在前面几篇文章中,介绍了Spark的启动流程Spark内核架构流程深度剖析,Spark源码分析之DAGScheduler详解,Spark源码解读之Executor以及Task工作原理剖析,Spark源 ...

最新文章

  1. 购买阿里云服务器地域如何选择?
  2. url传参参数编码的解码问题
  3. JAVA正则表达式:Pattern类与Matcher类详解
  4. 用回溯法找出n个自然数中取r个数的全排列
  5. 2019 live tex 发行版_TeX Live 2019安装指南
  6. 8盏流水灯反向闪烁c语言,课程设计(论文)_利用8255A芯片实现流水灯闪烁设计.doc...
  7. ping可以访问百度ip但不能访问百度域名|couldn't resolve host api.weixin.qq.com
  8. RHEL6.3基本网络配置(4) 其它常用网络配置文件
  9. H5U PLC定位控制功能块(EtherCAT总线)
  10. Ansible 学习总结(6)—— Ansible 19个常用模块使用示例
  11. ESP8266 读取MPU-6050数据OLED显示
  12. 寒冬已过,2023抓住IT复苏新机会
  13. 编程语言中一些令人抓狂的规则
  14. 论语 宪问篇(笔记)
  15. VR在今夜苏醒:华为千兆VR ONT的诺曼底登陆
  16. 连接请求被计算机拒绝访问,Windows 10共享打印机解决方案被拒绝访问
  17. 随机切换必应美图html代码,随机显示必应每日一张图片为背景网站技巧教程
  18. SCAU 2018 初出茅庐 题解
  19. 老夫整理的Java面经+题目(阿里、腾讯、头条、京东、IBM等等)佛渡有缘人
  20. 全国电子联行系统(EIS)、大额支付系统、

热门文章

  1. 加载远程图片_Cocos Creator工程JavaScript实现远程图片的加载
  2. 漫画:什么是ZooKeeper、Znode、最大ZXID、Paxos、ZAB协议?
  3. Charles 4.2 HTTPS抓包,乱码设置,证书信任,证书安装
  4. Docker 核心技术 NameSpace, CGroup, AUFS, DeviceMapper
  5. java 正则表达式 unicode_java正则表达式中的POSIX 字符类和Unicode 块和类别的类介绍...
  6. 459.重复的子字符串
  7. Computing the cost J(θ)----吴恩达机器学习作业
  8. 后台数据量太大传输慢_哪些因素会导致慢查询?
  9. css 点击效果_纯 CSS 实现吸附效果
  10. 凸优化有关的数值线性代数知识 4分块消元与Schur补