线程安全是并发编程中的重要关注点。
造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。
为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行。
在 Java 中,关键字 Synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)。
下面来一起探索Synchronized的基本使用、实现机制。
面试题来自:[社招一年半面经分享(含阿里美团头条京东滴滴)]
喜欢的话,之后会分享更多系列文章!
觉得有收获,希望帮忙点赞,转发下哈,谢谢,谢谢
两类锁:
对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)。
类锁:指Synchronized修饰静态的方法或指定锁为Class对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,而退出或抛出异常时必须释放锁。
代码示例如下:
public class SynchronizedTest {
/**
* 修饰静态方法, 等同于下面注释的方法
*/
public synchronized static void test1() {
System.out.println("月伴飞鱼");
}
// public static void test1() {
// synchronized (SynchronizedTest.class){
// System.out.println("月伴飞鱼");
// }
// }
/**
* 修饰实例方法, 等同于下面注释的方法
*/
public synchronized void test2(){
System.out.println("月伴飞鱼");
}
// public void test2(){
// synchronized (this){
// System.out.println("月伴飞鱼");
// }
// }
/**
* 修饰代码块
*/
public void test3(){
synchronized (this){
System.out.println("月伴飞鱼");
}
}
}
多线程访问同步方法的几种情况:
两个线程同时访问一个对象的同步方法。
由于同步方法锁使用的是this对象锁,同一个对象的this锁只有一把,两个线程同一时间只能有一个线程持有该锁,所以该方法将会串行运行。
两个线程访问的是两个对象的同步方法。
由于两个对象的this锁互不影响,Synchronized将不会起作用,所以该方法将会并行运行。
两个线程访问的是Synchronized的静态方法。
Synchronized修饰的静态方法获取的是当前类模板对象的锁,该锁只有一把,无论访问多少个该类对象的方法,都将串行执行。
同时访问同步方法与非同步方法
非同步方法不受影响。
访问同一个对象的不同的普通同步方法。
由于this对象锁只有一个,不同线程访问多个普通同步方法将串行运行。
同时访问静态Synchronized和非静态Synchronized方法
静态Synchronized方法的锁为class对象的锁,非静态Synchronized方法锁为this的锁,它们不是同一个锁,所以它们将并行运行。
大家在使用synchronized关键字的时候,可能经常会这么写:
synchronized (this) {
...
}
它的作用域是当前对象,锁的就是当前对象,谁拿到这个锁谁就可以运行它所控制的代码。
当有一个明确的对象作为锁时,就可以这么写,但是当没有一个明确的对象作为锁,只想让一段代码同步时,可以创建一个特殊的变量(对象)来充当锁:
public class Demo {
private final Object lock = new Object();
public void methonA() {
synchronized (lock) {
...
}
}
}
这样写没问题。但是用new Object()
作为锁对象是否是一个最佳选择呢?
我在StackOverFlow看到这么一篇文章:object-vs-byte0-as-lock
大意就是用new byte[0]
作为锁对象更好,会减少字节码操作的次数。
public class Demo {
private final byte[] lock = new byte[0];
}
具体细节大家,可以看看这篇文章,算提供一种思路吧!
因为Synchronized锁的是对象,在讲解原理之前先介绍下对象结构相关知识。
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。
对象头
对象头包括两部分信息:运行时数据Mark Word
和类型指针
如果对象是数组对象,那么对象头占用3个字宽(Word
)(需要记录数组长度),如果对象是非数组对象,那么对象头占用2个字宽(1word = 2Byte = 16bit
)
对象头的类型指针指向该对象的类元数据,虚拟机通过这个指针可以确定该对象是哪个类的实例
Mark Word
用于存储对象自身的运行时数据, 如哈希码(HashCode
)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。
这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits。
Synchronized锁对象就存储在MarkWord中,下面是MarkWord的布局:
32位虚拟机
实例数据
实例数据就是在程序代码中所定义的各种类型的字段,包括从父类继承的
对齐填充
由于HotSpot
的自动内存管理要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,对象头的数据正好是8的整数倍,所以当实例数据不够8字节整数倍时,需要通过对齐填充进行补全
意思是每次分配的内存大小一定是8的倍数,如果对象头+实例数据的值不是8的倍数,那么会重新计算一个较大值,进行分配
下面的代码,在命令行执行 javac,然后再执行javap -v -p
,就可以看到它具体的字节码。
可以看到,在字节码的体现上,它只给方法加了一个 flag:ACC_SYNCHRONIZED
。
synchronized void syncMethod() {
System.out.println("syncMethod");
}
synchronized void syncMethod();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4
3: ldc #5
5: invokevirtual #6
8: return
我们再来看下同步代码块的字节码。可以看到,字节码是通过 monitorenter 和monitorexit 两个指令进行控制的。
void syncBlock(){
synchronized (Test.class){
}
}
void syncBlock();
descriptor: ()V
flags:
Code:
stack=2, locals=3, args_size=1
0: ldc #2
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit // 两个monitorexit是表示正常退出和异常退出的场景
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。
其中在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp
文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; // 用来记录该对象被线程获取锁的次数,这也说明了synchronized是可重入的
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet,调用了wait方法之后会进入这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
每个 Java 对象在 JVM 的对等对象的头中保存锁状态,指向 ObjectMonitor。
ObjectMonitor 保存了当前持有锁的线程引用,EntryList 中保存目前等待获取锁的线程,WaitSet 保存 wait 的线程。
还有一个计数器count,每当线程获得 monitor 锁,计数器 +1,当线程重入此锁时,计数器还会 +1。当计数器不为 0 时,其它尝试获取 monitor 锁的线程将会被保存到EntryList中,并被阻塞。
当持有锁的线程释放了monitor 锁后,计数器 -1。当计数器归位为 0 时,所有 EntryList 中的线程会尝试去获取锁,但只会有一个线程会成功,没有成功的线程仍旧保存在 EntryList 中。
详细流程:
_EntryList
队列阻塞等待。_owner
为空,则从队列中移出并赋值与_owner
。_WaitSet
队列。我们都知道wait方法会释放monitor锁,即将_owner
赋值为null并进入_WaitSet
队列阻塞等待。这时其他在_EntryList
中的线程就可以获取锁了。_WaitSet
中的某个线程,这个线程就会再次尝试获取monitor锁。如果成功,则就会成为monitor的owner。Java对象如何与Monitor关联
相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 Synchronized 内置锁的性能进行了很多优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等。
自适应的自旋锁
在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。
比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。
锁消除
经过逃逸分析之后,如果发现某些对象不可能被其他线程访问到,那么就可以把它们当成栈上数据,栈上数据由于只有本线程可以访问,自然是线程安全的,也就无需加锁,所以会把这样的锁给自动去除掉。
锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
偏向锁/轻量级锁/重量级锁
JVM 默认会优先使用偏向锁,如果有必要的话才逐步升级,这大幅提高了锁的性能。
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
从JDK 1.6中默认是开启偏向锁,可以通过-XX:-UseBiasedLocking
来禁用偏向锁
偏向锁
在只有一个线程使用了锁的情况下,偏向锁能够保证更高的效率。
具体过程是这样的:当第一个线程第一次访问同步块时,会先检测对象头 Mark Word 中的标志位 Tag 是否为 01,以此判断此时对象锁是否处于无锁状态或者偏向锁状态。
线程一旦获取了这把锁,就会把自己的线程 ID 写到 MarkWord 中,在其他线程来获取这把锁之前,锁都处于偏向锁状态。
当下一个线程参与到偏向锁竞争时,会先判断 MarkWord 中保存的线程 ID 是否与这个线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。
轻量级锁
当锁处于轻量级锁的状态时,就不能够再通过简单地对比标志位 Tag 的值进行判断,每次对锁的获取,都需要通过自旋。
当然,自旋也是面向不存在锁竞争的场景,比如一个线程运行完了,另外一个线程去获取这把锁;但如果自旋失败达到一定的次数,锁就会膨胀为重量级锁。
重量级锁
重量级锁,这种情况下,线程会挂起,进入到操作系统内核态,等待操作系统的调度,然后再映射回用户态。系统调用是昂贵的,所以重量级锁的名称由此而来。
如果系统的共享变量竞争非常激烈,锁会迅速膨胀到重量级锁,这些优化就名存实亡。
如果并发非常严重,可以通过参数-XX:-UseBiasedLocking
禁用偏向锁,理论上会有一些性能提升,但实际上并不确定。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 基本没有线程竞争锁的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 适用于少量线程竞争锁对象,且线程持有锁时间不长,追求响应时间的场景。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量。竞争比较激烈,锁持有时间较长的场景 |
Synchronized和Lock的区别:
monitorenter
和 monitorexit 两个指令实现,Lock是API层面的东西,JUC提供的具体类lockInterruptibly
,调用interrupt方法可中断本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Q5ZGv2wjMecWedezADGHgQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。