使用 Synchronized 关键字
使用 Synchronized 关键字来解决并发问题是最简单的一种方式,我们只需要使用它修饰需要被并发处理的代码块、方法或字段属性,虚拟机自动为它加锁和释放锁,并将不能获得锁的线程阻塞在相应的阻塞队列上。
基本使用
我们在上篇文章介绍线程的基本概念时,提到了多线程的好处,能够最大化 CPU 使用效率、更友好交互等等,但是也提出了它带来的问题,比如竞态条件、内存可见性问题。
我们引用上篇文章中的一个案例:
一百个线程随机地为 count 加一,由于自增操作非原子性,多线程之间不正常的访问导致 count 最终的值不确定,始终得不到预期的结果。
使用 synchronized 即刻就能解决,看代码:
代码稍作修改,现在的程序无论你运行多少次,或者你增大并发量,最后 count 的值总是正确的 100 。
大概什么意思呢?
我们的 JAVA 中,对于每个对象都有一把『内置锁』,而 synchronized 中的代码在被线程执行之前,会去尝试获取一个对象的锁,如果成功,就进入并顺利执行代码,否则将会被阻塞在该对象上。
除此之外,synchronized 除了可以修饰代码块,还可以直接修饰在方法上,例如:
public synchronized void addCount(){......}
public static synchronized void addCount(){......}
这是两种不同的使用方式,前一种是使用 synchronized 修饰的实例方法,那么 synchronized 使用的就是当前方法调用时所属的那个实例的『内置锁』。也就是说,addCount 方法调用前会去尝试获取调用实例对象的锁。
而后一种 addCount 方法是一个静态方法,所以 synchronized 使用的就是 addCount 所属的类对象的锁。
synchronized 的使用方式还是很简单的,什么时候加锁,什么时候释放锁都不需要我们操心,被 JVM 封装好了,下面我们就来简单看看 JVM 是如何实现这种间接锁机制的。
基本实现原理
我们先看一段简单的代码:
public class TestAxiom {
private int count;
@Test
public void test() throws InterruptedException {
synchronized (this){
count++;
}
}
}
这是一段非常简单的代码,使用 synchronized 修饰代码块,保护 count++ 操作。现在我们反编译一下:
可以看到,在执行 count++ 指令之前,编译器加了一条 monitorenter 指令,count++ 指令执行结束时又加了一条 monitorexit 指令。准确意义上来说,这就是两条加锁的释放锁的指令,具体细节我们稍后再看。
除此之外,我们的 synchronized 方法在反编译后并没有这两条指令,但是编译器却在方法表的 flags 属性中设置了一个标志位 ACC_SYNCHRONIZED。
这样,每个线程在调用该方法之前都会检查这个状态位是否为 1,如果状态为 1 说明这是一个同步方法,需要首先执行 monitorenter 指令去尝试获取当前实例对象的内置锁,并在方法执行结束执行 monitorexit 指令去释放锁。
其实本质上是一样的,只是 synchronized 方法是一种隐式的实现。下面我们来看一看这个内置锁的具体细节。
Java 中一个对象主要由以下三种类型数据组成:
- 对象头:也称 Mark Word,主要存储的对象的 hash 值以及相关锁信息。
- 实例数据:保存的当前对象的数据,包括父类属性信息等。
- 填充数据:这部分是应 JVM 要求,每个对象的起始地址必须是 8 的倍数,所以如果当前对象不足 8 的倍数字节时用于字节填充。
我们的『内置锁』在对象头里面,而 Mark Word 的一个基本结构是这样的:
先不去管什么是,轻量锁,重量锁,偏向锁,自旋锁,这是虚拟机一种锁优化机制,通过锁膨胀来优化性能,这一点的细节我们以后再介绍,你先把它们统一理解为一把锁。
其中,每把锁会有一个标志位用于区分锁类型,和一个指向锁记录的指针,也就是说锁指针会关联另一种结构,Monitor Record。
Owner 字段存储的是拥有当前锁的线程唯一标识号,当某个线程拥有了该锁之后就会把自己的线程号写入这个字段中。如果某个线程发现这里的 Owner 字段不是 null 也不是自己的线程号,那么它将会被阻塞在 Monitor 的阻塞队列上直至某个线程走出同步代码块并发起唤醒操作。
总结一下,被 synchronized 修饰的代码块或者方法在编译器会被额外插入两条指令,monitorenter 会去检查对象头锁信息,对应到一个 Monitor 结构,如果该结构的 Owner 字段已经被占用了,那么当前线程将会被阻塞在 Monitor 的一个阻塞队列上,直到占有锁的线程释放了锁并唤起一波新的锁竞争。
synchronized 的几个特性
1、可重入性
一个对象往往有多个方法,这些方法有的是同步的,有的是非同步的,那么如果一个线程已经获得了某个对象的锁并进入了其某个同步方法,而这个同步方法中还需要调用同一实例的另一个同步方法,是否需要重新竞争锁?
这对于某些锁来说,是需要重新竞争锁的,但是我们的 synchronized 是「可重入的」,也就是说,如果当前线程获得了某个对象的锁,那么该对象的所有方法都是可以无需竞争锁式调用的。
原因也很简单,monitorenter 指令找到 Monitor,查看了 Owner 字段的值等于当前线程的线程号,于是将 Nest 字段增加一,表示当前线程多次持有该对象的锁,每调用一次 monitorexit 都会减一 Nest 的值。
2、内存可见性
引用上篇文章的一个例子:
线程 ThreadTwo 不停的监听 flag 的值,而我们主线程对 flag 进行了修改,由于内存可见性,ThreadTwo 看不见,于是程序一直死循环。
某种意义上,synchronized 是可以解决这类内存可见性问题的,修改代码如下:
主线程先获得 obj 的内置锁,然后启动 ThreadTwo 线程,该线程由于获取不到 obj 的锁而被阻塞,也就是它知道已经有其他线程在操作共享变量,所以等到自己获得锁的时候一定要从内存重新读一下共享变量。
而我们的主线程会在释放锁的时候将私有工作内存中所有的全局变量的值刷新到内存空间,这样其实就实现了多线程之间的内存可见性。
当然有一点大家要注意,synchronized 修饰的代码块会在释放锁的时候刷新自己更改过的全局变量,但是另一个线程要想看见,必须也从内存中重新读才行。而一般情况下,不是你加了 synchronized 线程就会从内存中读数据的,而只有它在竞争某把锁失败后,得知有其他线程正在修改共享变量,这样的前提下等到自己拥有锁之后才会重新去刷内存数据。
你也可以试试,让 ThreadTwo 线程不去竞争 obj 这把锁,而随便给它一个对象,结果依然会是死循环,flag 的值只会是 ThreadTwo 刚启动时从内存读入的初始数据的缓存版。
但是说实话,解决内存可见性而使用 synchronized 代价太高,需要加锁和释放锁,甚至还需要阻塞和唤醒线程,我们一般使用关键字 volatile 直接修饰在变量上就可以了,这样对于该变量的读取和修改都是直接映射内存的,不经过线程本地私有工作内存的。
关于 synchronized 关键字我们暂时先介绍到这,后续还会涉及到它的,我们还要介绍近几个 JDK 版本对于 synchronized 的优化细节,包括自旋锁,偏向锁,重量级锁之间的锁膨胀机制,也是这种优化使得现在的 synchronized 性能不输于 Lock。
文章中的所有代码、图片、文件都云存储在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
欢迎关注微信公众号:OneJavaCoder,所有文章都将同步在公众号上。
使用 Synchronized 关键字的更多相关文章
- 从JAVA看C#中volatile和synchronized关键字的作用
最近一直在想C#中 volatile关键字到底是用来干什么的?查了很多.NET的文章都是说用volatile修饰的变量可以让多线程同时修改,这是什么鬼... 然后查到了下面这篇JAVA中关于volat ...
- Java多线程系列--“基础篇”04之 synchronized关键字
概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...
- java中synchronized关键字的用法
在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,下面看看这个关键字的用法. 因为synchronized关键字涉及到锁的概念,所以先来了解一些相关的锁知识. j ...
- 深入理解java中的synchronized关键字
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D ...
- synchronized关键字,Lock接口以及可重入锁ReentrantLock
多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...
- Java synchronized关键字用法(清晰易懂)
本篇随笔主要介绍 java 中 synchronized 关键字常用法,主要有以下四个方面: 1.实例方法同步 2.静态方法同步 3.实例方法中同步块 4.静态方法中同步块 我觉得在学习synchro ...
- volatile与synchronized关键字
volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatil ...
- Java 多线程 —— synchronized关键字
java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...
- 【深入比较ThreadLocal模式与synchronized关键字】
[深入比较ThreadLocal模式与synchronized关键字]ThreadLocal模式与synchronized关键字都是用于处理多线程并发访问变量的问题.只是两者处理问题的角度和思路不同. ...
- java synchronized关键字浅探
synchronized 是 java 多线程编程中用于使线程之间的操作串行化的关键字.这种措施类似于数据库中使用排他锁实现并发控制,但是有所不同的是,数据库中是对数据对象加锁,而 java 则是对将 ...
随机推荐
- Linux学习---位运算符
<<.>> ① << 左移 乘以2^n m << n m*(2^n) eg:4: 0 0 1 0 0 8: 0 1 0 0 0 [数据.数字]移位 左 ...
- 实践中 XunSearch(讯搜)更新索引方案对比
检测PHP-SDK的运行条件(查看是否支持XunSearch) $prefix/sdk/php/util/RequiredCheck.php $prefix #替换成你的安装目录 使用 XunSe ...
- unic
在线考试 答题剩余时间0小时51分18秒 考生须知 1.本次考试结束后,剩余补考次数:2次 2.考试时间为60分钟,超时系统自动交卷 3.本次考试满分100分(5*20道),60分通过考试 1. (单 ...
- PID control
|—平滑化算法 |—PID控制—|—P控制器编程 |—PD控制编程 |—PID控制编程 |—参数优化 |—实验P.PD.PID对减小系统误差的作用 这里讨论怎么将路径转变成行动指令(生成平滑的路径), ...
- <<君主论>>读后感
“<君主论>与<圣经>齐名,被称为邪恶的圣经,这本书的立论基础是人性本恶论,他所描述的人性之恶,主要是以西欧资本原始积累背景下的现实社会中人的各种丑恶现象为蓝本的.” 要是高中 ...
- JavaScript自定义鼠标右键菜单
下面为JavaScript代码 window.onload = function () { //好友列表 var f = 0; //判断指定id的元素在页面中是否存在 if (document.get ...
- Openvswtich 学习笔记
场景: 创建一个Virtual Switch,支持VLAN,支持MAC-Learning 包含下面四个Port: P1, truck port P2, VLAN 20 P3, P4 VLAN 30 包 ...
- SpringBoot 通过 Exploded Archives 的方式部署
之前部署 SpringBoot 一直是用可执行 jar 的方式. java -jar codergroup-1.0.0.jar 就可以启动项目,为了能在后台运行,通常我们会使用这行命令 nohup j ...
- 给你的WordPress站点添加下雪特效
今天看到这个教程,感觉挺应景的,就自己尝试了下,效果还行,没截GIF图 方法: 该js文件已支持https,同时已将其及相关雪花图片进行CDN加速处理,可直接调用. 找到WordPress主题的foo ...
- springboot动态修改日志级别+权限认证
1. springboot动态修改日志级别+权限认证 1.1. 需求 网上找到的动态修改日志级别的方式,基本都是没有权限验证的,或者特地关闭权限验证,但也没给出加上验证的解决方式 修改日志等级也是一个 ...