synchronized 这个关键字,我相信对于并发编程有一定了解的人,一定会特别熟悉,对于一些可能在多线程环境下可能会有并发问题的代码,或者方法,直接加上synchronized,问题就搞定了。

  但是用归用,你明白它为什么要这么用?为什么就能解决我们所说的线程安全问题?

  下面,可乐将和大家一起深入的探讨这个关键字用法。

1、示例代码结果?

  首先大家看一段代码,大家想想最后的打印count结果是多少?

 1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
2
3
4 /**
5 * Create by ItCoke
6 */
7 public class SynchronizedTest implements Runnable{
8
9 public static int count = 0;
10
11 @Override
12 public void run() {
13 addCount();
14
15 }
16
17 public void addCount(){
18 int i = 0;
19 while (i++ < 100000) {
20 count++;
21 }
22 }
23
24 public static void main(String[] args) throws Exception{
25 SynchronizedTest obj = new SynchronizedTest();
26 Thread t1 = new Thread(obj);
27 Thread t2 = new Thread(obj);
28 t1.start();
29 t2.start();
30 t1.join();
31 t2.join();
32 System.out.println(count);
33
34 }
35
36
37 }

  代码很简单,主线程中启动两个线程t1和t2,分别调用 addCount() 方法,将count的值都加100000,然后调用 join() 方法,表示主线程等待这两个线程执行完毕。最后打印 count 的值。

  应该没有答案一定是 200000 的同学吧,很好,大家都具备一定的并发知识。

  这题的答案是一定小于等于 200000,至于原因也很好分析,比如 t1线程获取count的值为0,然后执行了加1操作,但是还没来得及同步到主内存,这时候t2线程去获取主内存的count值,发现还是0,然后继续自己的加1操作。也就是t1和t2都执行了加1操作,但是最后count的值依然是1。

  那么我们应该如何保证结果一定是 200000呢?答案就是用 synchronized。

2、修饰代码块

  直接上代码:

 1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
2
3
4 /**
5 * Create by ItCoke
6 */
7 public class SynchronizedTest implements Runnable{
8
9 public static int count = 0;
10
11 private Object objMonitor = new Object();
12
13 @Override
14 public void run() {
15 addCount();
16
17 }
18
19 public void addCount(){
20 synchronized (objMonitor){
21 int i = 0;
22 while (i++ < 100000) {
23 count++;
24 }
25 }
26
27 }
28
29 public static void main(String[] args) throws Exception{
30 SynchronizedTest obj = new SynchronizedTest();
31 Thread t1 = new Thread(obj);
32 Thread t2 = new Thread(obj);
33 t1.start();
34 t2.start();
35 t1.join();
36 t2.join();
37 System.out.println(count);
38
39 }
40
41
42 }

  我们在 addCount 方法体中增加了一个 synchronized 代码块,将里面的 while 循环包括在其中,保证同一时刻只能有一个线程进入这个循环去改变count的值。

3、修饰普通方法

 1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
2
3
4 /**
5 * Create by ItCoke
6 */
7 public class SynchronizedTest implements Runnable{
8
9 public static int count = 0;
10
11 private Object objMonitor = new Object();
12
13 @Override
14 public void run() {
15 addCount();
16
17 }
18
19 public synchronized void addCount(){
20 int i = 0;
21 while (i++ < 100000) {
22 count++;
23 }
24
25 }
26
27 public static void main(String[] args) throws Exception{
28 SynchronizedTest obj = new SynchronizedTest();
29 Thread t1 = new Thread(obj);
30 Thread t2 = new Thread(obj);
31 t1.start();
32 t2.start();
33 t1.join();
34 t2.join();
35 System.out.println(count);
36
37 }
38
39
40 }

  对比上面修饰代码块,直接将 synchronized 加到 addCount 方法中,也能解决线程安全问题。

4、修饰静态方法

  这个我们就不贴代码演示了,将 addCount() 声明为一个 static 修饰的方法,然后在加上 synchronized ,也能解决线程安全问题。

5、原子性、可见性、有序性

  通过 synchronized 修饰的方法或代码块,能够同时保证这段代码的原子性、可见性和有序性,进而能够保证这段代码的线程安全。

  比如通过 synchronized 修饰的代码块:

  

  其中 objMonitor 表示锁对象(下文会介绍这个锁对象),只有获取到这个锁对象之后,才能执行里面的代码,执行完毕之后,在释放这个锁对象。那么同一时刻就会只有一个线程去执行这段代码,把多线程变成了单线程,当然不会存在并发问题了。

  这个过程,大家可以想象在公司排队上厕所的情景。

  对于原子性,由于同一时刻单线程操作,肯定能够保证原子性。

  对于有序性,在JMM内存模型中的Happens-Before规定如下,所以也是能够保证有序性的。

程序的顺序性规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。

  最后对于可见性,JMM内存模型也规定了:

对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

  大家可能会奇怪,synchronized 并没有lock和unlock操作啊,怎么也能够保证可见性,大家不要急,其实JVM对于这个关键字已经隐式的实现了,下文看字节码会明白的。

6、锁对象

  大家要注意,我在通过synchronized修饰同步代码块时,使用了一个 Object 对象,名字叫 objMonitor。而对于修饰普通方法和静态方法时,只是在方法声明时说明了,并没有锁住什么对象,其实这三者都有各自的锁对象,只有获取了锁对象,线程才能进入执行里面的代码。

1、修饰代码块:锁定锁的是synchonized括号里配置的对象
2、修饰普通方法:锁定调用当前方法的this对象
3、修饰静态方法:锁定当前类的Class对象

  多个线程之间,如果要通过 synchronized 保证线程安全,获取的要是同一把锁。如果多个线程多把锁,那么就会有线程安全问题。如下:

 1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
2
3
4 /**
5 * Create by ItCoke
6 */
7 public class SynchronizedTest implements Runnable{
8
9 public static int count = 0;
10
11
12
13 @Override
14 public void run() {
15 addCount();
16
17 }
18
19 public void addCount(){
20 Object objMonitor = new Object();
21 synchronized(objMonitor){
22 int i = 0;
23 while (i++ < 100000) {
24 count++;
25 }
26 }
27 }
28
29 public static void main(String[] args) throws Exception{
30 SynchronizedTest obj = new SynchronizedTest();
31 Thread t1 = new Thread(obj);
32 Thread t2 = new Thread(obj);
33 t1.start();
34 t2.start();
35 t1.join();
36 t2.join();
37 System.out.println(count);
38
39 }
40
41
42 }

  我们把原来的锁 objMonitor 对象从全局变量移到 addCount() 方法中,那么每个线程进入每次进入addCount() 方法都会新建一个 objMonitor 对象,也就是多个线程用多把锁,肯定会有线程安全问题。

7、可重入

  可重入什么意思?字面意思就是一个线程获取到这个锁了,在未释放这把锁之前,还能进入获取锁,如下:

  

  在 addCount() 方法的 synchronized 代码块中继续调用 printCount() 方法,里面也有一个 synchronized ,而且都是获取的同一把锁——objMonitor。

  synchronized 是能够保证这段代码正确运行的。至于为什么具有这个特性,可以看下文的实现原理。

8、实现原理

  对于如下这段代码:

 1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest;
2
3 /**
4 * Create by YSOcean
5 */
6 public class SynchronizedByteClass {
7 Object objMonitor = new Object();
8
9 public synchronized void method1(){
10 System.out.println("Hello synchronized 1");
11 }
12
13 public synchronized static void method2(){
14 System.out.println("Hello synchronized 2");
15 }
16
17 public void method3(){
18 synchronized(objMonitor){
19 System.out.println("Hello synchronized 2");
20 }
21
22 }
23
24 public static void main(String[] args) {
25
26 }
27 }

  我们可以通过两种方法查看其class文件的汇编代码。

  ①、IDEA下载 jclasslib 插件

  

  然后点击 View——Show Bytecode With jclasslib

  

  ②、通过 javap 命令

javap -v 文件名(不要后缀)

  注意:这里生成汇编的命令是根据编译之后的字节码文件(class文件),所以要先编译。

  ③、修饰代码块汇编代码

  我们直接看method3() 的汇编代码:

  

  对于上图出现的 monitorenter 和 monitorexit 指令,我们查看 JVM虚拟机规范:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html,可以看到对这两个指令的介绍。

  下面我们说明一下这两个指令:

  一、monitorenter

  

  每个对象与一个监视器锁(monitor)关联。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1、如果 monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  二、monitorexit

  执行monitorexit的线程必须是object ref所对应的monitor的所有者。

  指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

  通过上面介绍,我们可以知道 synchronized 底层就是通过这两个命令来执行的同步机制,由此我们也可以看出synchronized 具有可重入性

  ③、修饰普通方法和静态方法汇编代码

  

  

  可以看到都是通过指令 ACC_SYNCHRONIZED 来控制的,虽然没有看到方法的同步并没有通过指令monitorenter和monitorexit来完成,但其本质也是通过这两条指令来实现。

  当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实和修饰代码块本质上没有区别,只是方法的同步是一种隐式的方式来实现。

9、异常自动unlock

  可能会有细心的朋友发现,我在介绍 synchronized 修饰代码块时,给出的汇编代码,用红框圈住了两个 monitorexit,根据我们前面介绍,获取monitor加1,退出monitor减1,等于0时,就没有锁了。那为啥会有两个 monitorexit,而只有一个 monitorenter 呢?

  

  第 6 行执行 monitorenter,然后第16行执行monitorexit,然后执行第17行指令 goto 25,表示跳到第25行代码,第25行是 return,也就是直接结束了。

  那第20-24行代码中是什么意思呢?其中第 24 行指令 athrow 表示Java虚拟机隐式处理方法完成异常结束时的监视器退出,也就是执行发生异常了,然后去执行 monitorexit。

  进而可以得到结论:

synchronized 修饰的方法或代码块,在执行过程中抛出异常了,也能释放锁(unlock)

  我们可以看如下方法,手动抛出异常:

  

  然后获取其汇编代码,就只有一个 monitorexit 指令了。

  

Java关键字(八)——synchronized的更多相关文章

  1. java关键字:synchronized

    JAVA 如何共享资源 关于synchronized函数: java具有内置机制,可防止某种资源(此处指的是对象的内存内容)冲突.由于你通常会将某class的数据元素声明为private,并且只经由其 ...

  2. java关键字之synchronized

    1.synchronized可以用了修饰一个普通方法,或者代码块,这个时候synchronized锁定的是当前对象,只要有一个线程在访问对应的方法或代码块,其他线程必须等待.2.synchronize ...

  3. java 线程及synchronized关键字

         从本篇开始,我们将会逐渐总结关于java并发这一块的内容,也可以理解为是我的笔记,主要来自于一些博客和java书籍中的内容,所有的内容都是来自于他们之中并且加上了我自己的理解和认识.     ...

  4. Java多线程:synchronized关键字和Lock

    一.synchronized synchronized关键字可以用于声明方法,也可以用来声明代码块,下面分别看一下具体的场景(摘抄自<大型网站系统与Java中间件实践>) 案例一:其中fo ...

  5. 巨人大哥谈Java中的Synchronized关键字用法

    巨人大哥谈Java中的Synchronized关键字用法 认识synchronized 对于写多线程程序的人来说,经常碰到的就是并发问题,对于容易出现并发问题的地方价格synchronized基本上就 ...

  6. Java进阶1. Synchronized 关键字

    Java进阶1. Synchronized 关键字 20131025 1.关于synchronized的简介: Synchronized 关键字代表对这个方法加锁,相当于不管那一个线程,运行到这个方法 ...

  7. Java并发之synchronized关键字深度解析(二)

    前言 本文继续[Java并发之synchronized关键字深度解析(一)]一文而来,着重介绍synchronized几种锁的特性. 一.对象头结构及锁状态标识 synchronized关键字是如何实 ...

  8. 并发系列2:Java并发的基石,volatile关键字、synchronized关键字、乐观锁CAS操作

    由并发大师Doug Lea操刀的并发包Concurrent是并发编程的重要包,而并发包的基石又是volatile关键字.synchronized关键字.乐观锁CAS操作这些基础.因此了解他们的原理对我 ...

  9. Java关键字总结及详解

    Java关键字是Java的保留字,这些保留字不能用来作为常量.变量.类名.方法名及其他一切标识符的名称. 一.基本数据类型 Java中有八种基本数据类型,六种数字类型(四个整数型.六中浮点型),一种字 ...

随机推荐

  1. python那些需要知道的事儿——逻辑运算与比大小

    一.逻辑运算 逻辑运算符: and   or   not,结果为布尔值(True和False) 1.基本逻辑运算符介绍 not :将后面的逻辑运算结果取反 >>> not 1 < ...

  2. 面试高频题:说一说对Spring和SpringMvc父子容器的理解?

    引言 以前写了几篇关于SpringBoot的文章<面试高频题:springBoot自动装配的原理你能说出来吗>.<保姆级教程,手把手教你实现一个SpringBoot的starter& ...

  3. C++并发与多线程学习笔记--单例设计模式、共享数据分析

    设计模式 共享数据分析 call_once 设计模式 开发程序中的一些特殊写法,这些写法和常规写法不一样,但是程序灵活,维护起来方便,别人接管起来,阅读代码的时候都会很痛苦.用设计模式理念写出来的代码 ...

  4. Recoil 默认值及数据级联的使用

    Recoil 中默认值及数据间的依赖 通过 Atom 可方便地设置数据的默认值, const fontSizeState = atom({ key: 'fontSizeState', default: ...

  5. 阿里巴巴面试-Java后端-社招5面技术总结(Offer已拿)

    最近接到阿里妈妈的面试通知,历经一个月,虽然过程挺坎坷,但总算是拿到了offer.这里简单记录下面试所遇问题,仅供各位大佬参考. 由于前面两面的时间过去的有点久了,只能根据记忆大概写些记得问题. 部门 ...

  6. linux中[gcc -shared -fPIC]的含义

    linux在gcc编译时加上 -shared 参数时,目的是使源码编译成动态库 .so 文件: 而-fPIC的作用是 告知编译器 生成位置无关代码(编译产生的代码没有绝对位置,只有相对位置):从而可以 ...

  7. Go之Zap日志库集成Gin

    简介 在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能: 1 . 能够将事件记录到文件中,而不是应用程序控制台; 2 . 日志切割-能够根据文件大小.时间或间隔等来切割日志文件; ...

  8. 看从小自带BUFF的他,如何用代码降低万物互联的门槛

    摘要:3岁下象棋,5岁解方程!他说物联网开发就像打怪玩游戏 本文分享自华为云社区<HDC.Cloud2021 | 华为云IoT开发者:用代码降低万物互联的门槛>,原文作者:华为云头条 . ...

  9. 构建基于表单配置的 Jenkins 测试项目(接口、UI、APP、Jmeter)

    1. 第一个 hello world 项目 2. 构建自动触发的项目(接口测试) 1)新建测试项目(执行测试脚本) 2)新建 Maven 打包项目 3)手动执行构建 4)修改 Web 工程代码并 pu ...

  10. Jenkins 系统管理与配置

    1. Jenkins 安装插件的两种方式 2. 添加凭据(Credentials) 3. 系统管理--全局工具配置 4. 系统管理--系统设置 5. 常用插件说明 Extended E-mail No ...