深入浅出synchronized关键字
最近作者比较忙,所以断更了一段时间,也是因为作者前一段时间离职和最近刚入职新公司很多东西还不是很熟,在新公司摸索和学习中,最近忙里偷闲输出一篇技术文章
上一篇文章我们从硬件层面到软件层面讲解了volatile的由来和原理,不知道大家有没有什么不懂的地方,有的可以评论区给我留言,这篇文章我将带着大家掀开
synchronized
的面纱,接下来大家就跟着我的思路走,gogogo?
为什么有synchronized?
为了更好的利用CPU的性能,所以引入了CPU多级缓存、进程和线程的概念;第一个就是多核CPU以及超线程技术来实现线程的并行执行;第二个就是线程的异步化执行相比于同步执行来说,异步执行能够有更好的性能和更高的吞吐量
但是这样也带来了一些问题,在CPU多级缓存的前提下,多线程访问共享变量会出现一些数据安全性的问题
思考如何保证线程并行的数据安全性问题
我们可以思考下,问题的本质是在于对共享变量的访问没有顺序,那么我们能不能使用一种方式使得线程对共享变量的操作变成串行呢
按照大家的惯性思维,最先想到的是不是锁呢?
毕竟这个场景大家并不陌生,我们在和数据库打交道的时候就应该有过一些了解,比如乐观锁、悲观锁的概念,那么什么是锁呢?它就是处理并发的一种同步手段,为了达到串行的目的,首先这个锁就要具有互斥的特征
Java提供的锁就是synchronized关键字和Lock(这个不做展开,这篇文章主要讲synchronized,Lock会在下一篇文章中详解)
synchronized的基本认识
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java1.6对synchronized进行了各种优化之后,它的性能就得到了很大的提升(从java1.8的ConcurrentHashMap底层就可以猜到,新版的synchronized性能真的不差!),Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续我们会慢慢展开
synchronized 的基本语法
synchronized 有三种方式来加锁,分别是
- 修饰实例方法,作用于当前实例加锁,进入同步代码前
要获得当前实例的锁 - 静态方法,作用于当前类对象加锁,进入同步代码前要
获得当前类对象的锁 - 修饰代码块,指定加锁对象,对给定对象加锁,进入同
步代码块前要获得给定对象的锁。
ps:不同的修饰,代表锁的控制粒度
synchronized的应用
public class Demo {
private static int count = 0;
public static void inc() {
synchronized (Demo.class) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Demo.inc()).start();
}
Thread.sleep(3000);
System.out.println("运行结果" + count);
}
}
复制代码
思考锁的如何存储的?
在上边的代码中我们可以看到仅仅通过一个关键字就能够实现多线程的互斥,那么为了实现线程的互斥特性,需要哪些因素和信息?
- 锁需要有一个标识来表示,比如的锁是什么状态,无锁还是有锁
- 同时这个状态需要对多个线程共享
接下来我们来分析,synchronized锁是如何储存的,在Java中synchronized是通过synchronized(lock)来基于lock这个对象的生命周期来进行控制锁的粒度的,那么是否锁状态的储存和lock对象有关呢?
接下来我们从对象是怎么在内存中储存的角度出发,去看看是通过对象里面的什么特性来实现锁的
在Hotspot虚拟机中,对象在内存中的布局可以分为三个部分:对象头、实例对象、填充域
ps:大家肯定会疑惑我是怎么知道的?
其实这些在Jvm级别的源代码中可以看到的,在这里我就不展开分析了,如果大家有想法或者想深究的,可以在评论区给我留言,我会把内容补充
为什么任何对象都可以实现锁
- 首先所有对象都是默认继承Object类,而每个Java Object类在Jvm内部都有一个native的C++对象与它对应的
- 线程在获取锁的时候,实际上就是获取一个监控器对象(monitor),monitor可以认为是一个同步对象,所有的对象都默认携带monitor对象,多个线程去访问同步代码块时,实际上就去争抢lock对象内部的monitor
synchronized锁的升级过程
在上面的图中,涉及到了偏向锁、同步锁(轻量级锁、重量级锁),接下来我们将讲解下这几种锁的区别和用途
hotspot虚拟机的作者发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争的情况,而且总是一个线程获取锁,所有基于这个概率,synchronized在jdk6以后做了一个优化,为了减少获取锁和释放锁的开销,引入偏向锁、轻量级锁的概念,所以大家可以发现,锁存在四种状态:无锁、偏向锁、轻量级锁、重量级锁,锁的状态是通过不断竞争而从低到高逐步升级的。
偏向锁
前面说过了,大部分情况下,锁不仅仅不存在竞争的情况,而且总是由一个线程获取,为了降低获取锁和释放锁的开销,虚拟机作者就引入了偏向锁的概念,怎么理解偏向锁呢?
当一个线程访问同步代码块时,会在对象头中存储当前的线程id,后续这个线程进入这段同步代码块,无需加锁和释放锁,就直接通过CAS比较对象头中的线程id就可以了
偏向锁的冲突
如果当前线程还未执行完,其他线程访问代码块比较线程id,发现不一致的情况,这样就产生了冲突的情况,这样锁就会升级成轻量级锁
一般Java都是用作服务端开发,绝大多数情况一定会存在2个以上的线程竞争,那么如果开启偏向锁,反而还要通过升级的过程,而带来资源消耗,所以可以通过Jvm参数来开启或者关闭偏向锁UseBiasedLocking
轻量级锁
轻量级锁其实就是在锁过程中用到了自旋锁
所谓自旋,就是指当有线程在执行代码块,当前线程就会原地循环等待,而不是把该线程给阻塞,直到那个线程释放锁,当前线程就可以获取锁
ps:自旋是会消耗CPU性能的,就可以类似执行一个空for循环,所以轻量级锁适合同步代码块能很快执行完毕的场景,这样原地等待很短的时间就可以获取锁了
自旋锁的使用其实一个参考了很多应用和背景的,大部分情况同步代码块的执行时间都是很短的,这样看似无意义的自旋,却能提升锁的性能,但是有利也有弊,自旋必须要有条件进行控制,否则无限制的自旋那将是灾难!!
我们可以手动的控制次数,可以通过 preBlockSpin 来修改
在jdk6以后还引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据上一次在同一个锁的时间和锁的拥有者来决定的,这里就不展开了,这样能够大大提升性能
轻量级锁的冲突
如果自旋达到一定次数还未能获取锁,这样就更加剧了竞争,轻量级锁就会升级到重量级锁
重量级锁
当轻量级锁升级到了重量级锁之后,就意味着线程就只能被挂起等待唤醒了
public class SyncTest{
public static void main(String[] args) {
synchronized (SyncTest.class) {
System.out.println("test");
}
}
}
复制代码
把上面的代码进行反编译后,可以看到它执行的字节码指令我们可以看到monitorenter和monitorexit这两个指令
在上面我们说每个Java对象都会有一个monitor关联,其实每个线程争抢锁就是在争抢这个monitor对象,monitorenter就是去获取一个monitor,monitorexit就是去释放monitor,其他线程就可以去争夺这个monitor
monitor是依赖操作系统的MutexLock(互斥锁)来实现的,线程如果被阻塞就会交给内核(Unix),如果频繁的从用户态(从阻塞恢复争抢锁)和内核态(阻塞状态)进行切换,这样就会严重印象性能
每个线程都会通过monitorenter来获取monitor也就是来获取锁,当获取失败时,就会加入同步等待队列,当持有锁的线程释放锁后,会通知同步等待队列,进而唤醒等待队列中的线程重新争抢monitor
总结
其实对于synchronized关键字来说,我们的使用非常简单,Jvm设计者已经帮我们屏蔽了大多数的实现细节,我们可以很简单的使用它,但是这样不是一个技术人应该做的,我们应该知其然知其所以然,把底层原理和细节弄明白,今天给大家讲解了synchronized的使用和底层的实现,还有锁的升级过程,我最近也熟悉了新公司的节奏了,接下来每周都会给大家输出一些技术文章,下一篇应该是用Java代码实现的锁ReentrantLock,如果大家有特别希望作者讲解和分析的知识点也可以在评论区给我留言
最后送给大家一句话,努力加油吧,多年以后,你一定会感谢曾经那么努力的自己!
我是爱写代码的何同学,我们下期再见!
最后发一个题外内容,#快手内推#
1.免费三餐,下午茶(每天不重样)
2.每月房补2000元
3.顶配MacBookPro工作,技术岗配备外接显示器
4.16薪,年终丰厚,月底发当月工资,绝对不压工资
5.周末加班双倍工资
各岗位均有HC,base北京杭州深圳 有想法的可以私我或者加我QQ 784753715
不内推也可以加个联系方式,大家交流交流技术?