并发编程-synchronized解决原子性问题
并发编程-synchronized解决原子性问题
文章目录
- 并发编程-synchronized解决原子性问题
- 零、说在前面
- 一、线程安全问题
- 1.1 什么是线程安全问题
- 1.2 自增运算不是线程安全的
- 1.3 临界区资源与临界区代码段
- 二、synchronized 关键字的使用
- 2.1 synchronized 关键字作用
- 2.2 synchronized 内置锁如何使用
- 1、修饰方法(同步方法)
- 2、修饰代码快(同步代码快)
- 3、同步方法和同步代码块的区别与联系
- 2.3 synchronized 内置锁分类
- 1、对象锁-代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁
- 2、对象锁-方法锁形式:synchronized修饰普通方法,锁对象默认为this
- 3、类锁-synchronize修饰静态方法
- 4、类锁-synchronized指定锁对象为Class对象
- 2.4 synchronized 内置锁释放
- 2.5 ynchronized 使用不当带来的死锁
- 1、死锁案例
- 2、死锁产生的必要条件
- 3、如何解决死锁问题
- 三、Java 对象结构与内置锁
- 3.1 Java对象结构
- 3.2 Mark Word 的结构信息
- 3.3 无锁、偏向锁、轻量级锁和重量级锁
- 四、偏向锁的原理与实战
- 4.1 偏向锁的核心原理
- 4.2 偏向锁的演示案例
- 4.3 偏向锁获取锁流程
- 4.4 偏向锁的撤销和膨胀
- 1、偏向锁的撤销
- 2、偏向锁的膨胀
- 五、轻量级锁的原理与实战
- 5.1 轻量级锁的核心原理
- 5.2 轻量级锁的案例演示
- 5.3 轻量级锁获取锁流程
- 5.4 轻量级锁的释放流程
- 5.5 轻量级锁和偏向锁的对比
- 六、重量级锁的原理与实战
- 6.1 重量级锁的核心原理
- 1、监视器原理
- 2、监视器ObjectMonitor
- 3、ObjectMonitor 的内部抢锁过程
- 4、重量级锁的实现流程
- 6.2 内核态和用户态
- 1、内核态
- 2、用户态
- 3、用户态内核态切换
- 4、pthread_mutex_lock系统调用
- 6.3 重量级锁的演示案例
- 七、锁升级以及各种锁的对比
- 7.1 锁升级的实现流程
- 7.2 偏向锁、轻量级锁、重量级锁的对比
- 八、Synchronized与Lock对比
- 8.1 synchronized的缺陷
- 1、效率低
- 2、不够灵活
- 3、无法知道是否成功获得锁
- 8.2 Lock解决相应问题
零、说在前面
有必要为友友们推荐一款极简的原生态AI:阿水AI6,需不需要都点点看看:👇👇👇
https://ai.ashuiai.com/auth/register?inviteCode=XT16BKSO3S
先看看美景养养眼,再继续以下乏味的学习,内容有点多,建议收藏分多次食用。
一、线程安全问题
1.1 什么是线程安全问题
当多个线程并发访问某个Java对象(Object)时,无论系统如何调度这些线程,也不论这些线程如何交替操作,这个对象都能表现出一致的、正确的行为,那么对这个对象的操作是线程安全的。如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。
1.2 自增运算不是线程安全的
使用10个线程并行运行,对一个共享数据进行自增运算,每个线程自增运算1000次,具体代码如下:
public class PlusTest {final int MAX_TREAD = 10;final int MAX_TURN = 1000;CountDownLatch latch = new CountDownLatch(MAX_TREAD);/*** 测试用例:测试不安全的累加器*/@org.junit.Testpublic void testNotSafePlus() throws InterruptedException {NotSafePlus counter = new NotSafePlus();Runnable runnable = () ->{for (int i = 0; i < MAX_TURN; i++) {counter.selfPlus();}latch.countDown();};for (int i = 0; i < MAX_TREAD; i++) {new Thread(runnable).start();}latch.await();Print.tcfo("理论结果:" + MAX_TURN * MAX_TREAD);Print.tcfo("实际结果:" + counter.getAmount());Print.tcfo("差距是:" + (MAX_TURN * MAX_TREAD - counter.getAmount()));}}public class NotSafePlus {private Integer amount = 0;//自增public void selfPlus() {amount++;}public Integer getAmount() {return amount;}}
运行结果:
[main|PlusTest.testNotSafePlus]:理论结果:10000
[main|PlusTest.testNotSafePlus]:实际结果:3557
[main|PlusTest.testNotSafePlus]:差距是:6443
结果分析:
为什么自增运算不是线程安全的呢?实际上,一个自增运算符是一个复合操作,至少包括三个JVM指令:“内存取值”“寄存器增加1”“存值到内存”。这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。
比如在amount=100时,假设有三个线程同一时间读取amount值,读到的都是100,增加1后结果为101,三个线程都将结果存入到amount的内存,amount的结果是101,而不是103。
“内存取值”“寄存器增加1”“存值到内存”这三个JVM指令是不可以再分的,它们都具备原子性,是线程安全的,也叫原子操作。但是,两个或者两个以上的原子操作合在一起进行操作就不再具备原子性。比如先读后写,就有可能在读之后,其实这个变量被修改了,就出现了数据不一致的情况。
1.3 临界区资源与临界区代码段
临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。
在并发情况下,临界区资源是受保护的对象。临界区代码段(Critical Section)是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。
二、synchronized 关键字的使用
2.1 synchronized 关键字作用
在Java中,线程同步使用最多的方法是使用synchronized关键字。每个Java对象都隐含有一把锁,这里称为Java内置锁(或者对象锁、隐式锁)。使用synchronized(syncObject)调用相当于获取syncObject的内置锁,所以可以使用内置锁对临界区代码段进行排他性保护。
解决原子性问题
2.2 synchronized 内置锁如何使用
1、修饰方法(同步方法)
synchronized关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法。在方法声明中设置synchronized同步关键字,保证了其方法的代码执行流程是排他性的。任何时间只允许一条线程进入同步方法(临界区代码段),如果其他线程都需要执行同一个方法,那么只能等待和排队。
//同步方法
public synchronized void selfPlus()
{amount++;
}
2、修饰代码快(同步代码快)
如果方法中内容太多,还继续使用同步方法,则会影响执行效率。为了执行效率,最好将同步方法分为小的临界区代码段。
将synchronized加在方法上,如果其保护的临界区代码段包含的临界区资源(要求是相互独立的)多于一个,会造成临界区资源的闲置等待,这就会影响临界区代码段的吞吐量。为了提升吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。synchronized同步块的写法是:
synchronized(syncObject) //同步块而不是方法
{//临界区代码段的代码块
}
在synchronized同步块后边的括号中是一个syncObject对象,代表着进入临界区代码段需要获取syncObject对象的监视锁,或者说将syncObject对象监视锁作为临界区代码段的同步锁。由于每一个Java对象都有一把监视锁(Monitor),因此任何Java对象都能作为synchronized的同步锁。单个线程在synchronized同步块后边同步锁后,方能进入临界区代码段;反过来说,当一条线程获得syncObject对象的监视锁后,其他线程就只能等待。
3、同步方法和同步代码块的区别与联系
区别:
synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法;而synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多条线程并发访问的。在一个方法中,并不一定所有代码都是临界区代码段,可能只有几行代码会涉及线程同步问题。所以synchronized代码块比synchronized方法更加细粒度地控制了多条线程的同步访问。
联系:
在Java的内部实现上,synchronized方法实际上等同于用一个synchronized代码块,这个代码块包含了同步方法中的所有语句,然后在synchronized代码块的括号中传入this关键字,使用this对象锁作为进入临界区的同步锁。
synchronized方法的同步锁实质上使用了this对象锁,这样就免去了手工设置同步锁的工作。而使用synchronized代码块需要手工设置同步锁。
2.3 synchronized 内置锁分类
1、对象锁-代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁
//示例1
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instance = new SynchronizedObjectLock();@Overridepublic void run() {// 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行synchronized (this) {System.out.println("我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "结束");}}public static void main(String[] args) {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();}
}// 示例2
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instance = new SynchronizedObjectLock();// 创建2把锁Object block1 = new Object();Object block2 = new Object();@Overridepublic void run() {// 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行synchronized (block1) {System.out.println("block1锁,我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");}synchronized (block2) {System.out.println("block2锁,我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");}}public static void main(String[] args) {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();}
}
2、对象锁-方法锁形式:synchronized修饰普通方法,锁对象默认为this
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instance = new SynchronizedObjectLock();@Overridepublic void run() {method();}public synchronized void method() {System.out.println("我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "结束");}public static void main(String[] args) {Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();}
}
3、类锁-synchronize修饰静态方法
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instance1 = new SynchronizedObjectLock();static SynchronizedObjectLock instance2 = new SynchronizedObjectLock();@Overridepublic void run() {method();}// synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把public static synchronized void method() {System.out.println("我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "结束");}public static void main(String[] args) {Thread t1 = new Thread(instance1);Thread t2 = new Thread(instance2);t1.start();t2.start();}
}
4、类锁-synchronized指定锁对象为Class对象
public class SynchronizedObjectLock implements Runnable {static SynchronizedObjectLock instance1 = new SynchronizedObjectLock();static SynchronizedObjectLock instance2 = new SynchronizedObjectLock();@Overridepublic void run() {// 所有线程需要的锁都是同一把synchronized(SynchronizedObjectLock.class){System.out.println("我是线程" + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "结束");}}public static void main(String[] args) {Thread t1 = new Thread(instance1);Thread t2 = new Thread(instance2);t1.start();t2.start();}
}
2.4 synchronized 内置锁释放
使用synchronized块时不必担心监视锁的释放问题,同步代码块正确执行成功之后,监视锁会自动释放;如果程序出现异常,监视锁也会自动释放。
2.5 ynchronized 使用不当带来的死锁
synchronized同步锁虽然能够解决线程安全问题,但是如果使用不当,就会导致死锁,即请求被阻塞一直无法返回。
死锁线程:
两个或者两个以上的线程在执行过程中,由于争夺同一个共享资源造成的相互等待的现象,在没有外部干预的情况下,这些线程将会一直阻塞无法往下执行,这些一直处于相互等待资源的线程就称为死锁线程。
1、死锁案例
定义一个资源类,提供如下两个方法,这两个方法都加了synchronized对象锁。
- saveResource()方法,用于保存资源。
- statisticsResource()方法,用于统计资源数量。
演示死锁代码如下:
两个线程分别访问两个不同的Resource对象,每个resource对象分别调用saveResource()方法保存resource对象的资源,这必然会导致死锁问题。由于两个线程持有自己的对象锁资源,在saveResource()方法中访问对方的statisticsResource()方法并占用对方的锁资源,所以产生互相等待造成死锁的现象。
2、死锁产生的必要条件
不管是线程级别的死锁,还是数据库级别的死锁,只能通过人工干预去解决,所以我们要在写程序的时候提前预防死锁的问题。导致死锁的条件有四个,这四个条件同时满足就会产生死锁。
3、如何解决死锁问题
按照前面说的四个死锁的发生条件,我们只需要破坏其中任意一个,就可以避免死锁的产生。其中,互斥条件我们不可以破坏,因为这是互斥锁的基本约束,其他三个条件都可以破坏。
4、
三、Java 对象结构与内置锁
3.1 Java对象结构
一个Java对象在JVM中的存储结构如下:
3.2 Mark Word 的结构信息
Java对象对象头中的Mark Word存储内容如下:
3.3 无锁、偏向锁、轻量级锁和重量级锁
synchronized的不同锁类型如下:
在Java对象的Mark Word中不同的信息表示不同的锁,具体信息如下:
由biased 和lock位的不同值,分别表示无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
四、偏向锁的原理与实战
4.1 偏向锁的核心原理
为什么要引入偏向锁?偏向锁的核心原理和核心思想是什么?引入偏向锁有什么缺点?
Java偏向锁是Java6引入的一项多线程优化。顾名思义,它会偏向于第一个访问锁对象的线程,如果同步锁只有一个线程访问,则线程是不需要触发同步的,这种情况下,就会给该线程加一个偏向锁;如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级到轻量级锁,然后再唤醒原持有偏向锁的线程。
4.2 偏向锁的演示案例
偏向锁的案例代码如下:
public class InnerLockTest {final int MAX_TREAD = 10;final int MAX_TURN = 1000;CountDownLatch latch = new CountDownLatch(MAX_TREAD);//偏向锁测试案例@org.junit.Testpublic void showBiasedLock() throws InterruptedException {Print.tcfo(VM.current().details());//JVM延迟偏向锁sleepMilliSeconds(5000);ObjectLock lock = new ObjectLock();Print.tcfo("抢占锁前, lock 的状态: ");lock.printObjectStruct();sleepMilliSeconds(5000);CountDownLatch latch = new CountDownLatch(1);Runnable runnable = () ->{for (int i = 0; i < MAX_TURN; i++) {synchronized (lock) {lock.increase();if (i == MAX_TURN / 2) {Print.tcfo("占有锁, lock 的状态: ");lock.printObjectStruct();//读取字符串型输入,阻塞线程
// Print.consoleInput();}}//每一次循环等待10mssleepMilliSeconds(10);}latch.countDown();};new Thread(runnable, "biased-demo-thread").start();//等待加锁线程执行完成latch.await();Print.tcfo("释放锁后, lock 的状态: ");lock.printObjectStruct();}}
4.3 偏向锁获取锁流程
偏向锁的获得锁的逻辑如下:
4.4 偏向锁的撤销和膨胀
假如有多个线程来竞争偏向锁,此对象锁已经有所偏向,其他的线程发现偏向锁并不是偏向自己,就说明存在了竞争,尝试撤销偏向锁(很可能引入安全点),然后膨胀到轻量级锁。
1、偏向锁的撤销
为什么调用Object.hashCode()或者System.identityHashCode()方法计算对象的哈希码之后,偏向锁将撤销?
偏向锁状态下Mark Word内容如下:
因为偏向锁没有存储Mark Word备份信息的地方。换句话说,因为对于一个对象其哈希码只会生成一次并保存在Mark Word中,偏向锁对象的Mark Word已经保存了线程ID,没有地方再保存哈希码时,所以只能撤销偏向锁,将Mark Word用于存放对象的哈希码。
轻量级锁会在帧栈的Lock Record(锁记录)中记录哈希码,重量级锁会在监视器中记录哈希码,起到了对哈希码备份的作用。而偏向锁没有地方备份哈希码,所以只能撤销偏向锁。调用哈希码计算将会使对象再也无法偏向,因为在Mark Word中已经放置了哈希码,偏向锁没有办法放置Thread ID了。调用哈希码计算后,当锁对象可偏向时,Mark Word将变成未锁定状态,并只能升级成轻量级锁;当对象正处于偏向锁时,调用哈希码将使偏向锁撤销后强制升级成重量锁。
偏向锁撤销的过程大致如下
2、偏向锁的膨胀
如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表明在这个对象锁上已经存在竞争了。JVM检查原来持有该对象锁的占有线程是否依然存活,如果挂了,就可以将对象变为无锁状态,然后进行重新偏向,偏向为抢锁线程。
如果JVM检查到原来的线程依然存活,就表明原来的线程还在使用偏执锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀(INFLATING)为轻量级锁。
五、轻量级锁的原理与实战
5.1 轻量级锁的核心原理
为什么引入轻量级锁?轻量级锁的核心原理是什么?
轻量级锁的加锁过程
-
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“ 01 ”状态,是否为偏向锁为“ 0 ”),虚拟机首先将在 当前线程的栈帧中 建立一个名为锁记录( Lock Record )的空间,用于存储锁对象目前的markword的拷贝,官方称之为Displaced Mark Word 。
-
拷贝对象头中的 markword 复制到锁记录中;
-
拷贝成功后,虚拟机将使用CAS操作尝试将对象的 markword 更新为指向LockRecord的指针,并将Lock Record里的 owner指针 指向对象的mark word。如果更新成功,则执行 步骤④ ,否则执行 步骤⑤ 。
-
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,将对象markword的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:
-
如果这个更新操作失败了,则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”, markword 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
抢锁线程在通过CAS自旋更新完Mark Word之后,还会做两个善后工作:
- 将含有锁对象信息(如哈希表等)的旧Mard Word值保存在抢锁线程Lock Record的DisplacedMark Word(可以理解为放错地方的Mark Word)字段中,这一步起到备份的作用,以便锁释放之后,将旧的Mark Word值恢复到锁对象头部。
- 抢锁线程将栈帧中的锁记录的owner指针指向锁对象。
锁记录是线程私有的,每个线程有自己的一份锁记录,在创建完锁记录后,会将内置锁对象的Mark Word拷贝到锁记录的Displaced Mark Word字段。这是为什么呢?因为内置锁对象的MarkWord的结构会有所变化,Mark Word将会出现一个指向锁记录的指针,而不再存着无锁状态下的锁对象哈希码等信息,所以必须将这些信息暂存起来,供后面在锁释放时使用。
5.2 轻量级锁的案例演示
轻量级锁演示代码如下
public class InnerLockTest {final int MAX_TREAD = 10;final int MAX_TURN = 1000;CountDownLatch latch = new CountDownLatch(MAX_TREAD);@org.junit.Testpublic void showLightweightLock() throws InterruptedException {Print.tcfo(VM.current().details());//JVM延迟偏向锁sleepMilliSeconds(5000);ObjectLock lock = new ObjectLock();Print.tcfo("抢占锁前, lock 的状态: ");lock.printObjectStruct();sleepMilliSeconds(5000);CountDownLatch latch = new CountDownLatch(2);Runnable runnable = () ->{for (int i = 0; i < MAX_TURN; i++) {synchronized (lock) {lock.increase();if (i == 1) {Print.tcfo("第一个线程占有锁, lock 的状态: ");lock.printObjectStruct();}}}//循环完毕latch.countDown();//线程虽然释放锁,但是一直存在for (int j = 0; ; j++) {//每一次循环等待1mssleepMilliSeconds(1);}};new Thread(runnable).start();sleepMilliSeconds(1000); //等待1sRunnable lightweightRunnable = () ->{for (int i = 0; i < MAX_TURN; i++) {synchronized (lock) {lock.increase();if (i == MAX_TURN / 2) {Print.tcfo("第二个线程占有锁, lock 的状态: ");lock.printObjectStruct();}//每一次循环等待1mssleepMilliSeconds(1);}}//循环完毕latch.countDown();};new Thread(lightweightRunnable).start();//等待加锁线程执行完成latch.await();sleepMilliSeconds(2000); //等待2sPrint.tcfo("释放锁后, lock 的状态: ");lock.printObjectStruct();}
}
5.3 轻量级锁获取锁流程
轻量级锁获取锁流程如下
轻量级锁的加锁原理如下
5.4 轻量级锁的释放流程
偏向锁也有锁释放的逻辑,但是它只是释放Lock Record,原本的偏向关系仍然存在,所以并不是真正意义上的锁释放。而轻量级锁释放之后,其他线程可以继续使用轻量级锁来抢占锁资源,具体的实现流程如下。
第一步,把Lock Record中_displaced_header存储的lock锁对象的Mark Word替换到lock锁对象的Mark Word中,这个过程会采用CAS来完成。
第二步,如果CAS成功,则轻量级锁释放完成。
第三步,如果CAS失败,说明释放锁的时候发生了竞争,就会触发锁膨胀,完成锁膨胀之后,再调用重量级锁的释放锁方法,完成锁的释放过程。
5.5 轻量级锁和偏向锁的对比
偏向锁,就是在一段时间内只由同一个线程来获得和释放锁,加锁的方式是把Thread Id保存到锁对象的Mark Word中。
轻量级锁,存在锁交替竞争的场景,在同一时刻不会有多个线程同时获得锁,它的实现方式是在每个线程的栈帧中分配一个BasicObjectLock对象(Lock Record),然后把锁对象中的Mark Word拷贝到Lock Record中,最后把锁对象的Mark Word的指针指向Lock Record。轻量级锁之所以这样设计,是因为锁对象在竞争的过程中有可能会发生变化,但是每个线程的Lock Record的Mark Word不会受到影响。因此当触发锁膨胀时,能够通过Lock Record和锁对象的Mark Word进行比较来判定在持有轻量级锁的过程中,锁对象是否被其他线程抢占过,如果有,则需要在轻量级锁释放锁的过程中唤醒被阻塞的其他线程。
六、重量级锁的原理与实战
6.1 重量级锁的核心原理
1、监视器原理
重量级锁原理
监视器特点
2、监视器ObjectMonitor
ObjectMonitor组件介绍
在 Hotspot虚拟 机中,监 视器是由 C++类 ObjectMonitor实现 的,ObjectMonitor类定 义在ObjectMonitor.hpp文件中,其构造器代码大致如下:
ObjectMonitor::ObjectMonitor() {_header = NULL;_count = 0;_waiters = 0,//线程的重入次数_recursions = 0;_object = NULL;//标识拥有该monitor的线程_owner = NULL;//等待线程组成的双向循环链表_WaitSet = NULL;_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;//多线程竞争锁进入时的单向链表cxq = NULL ;FreeNext = NULL ;//_owner从该双向循环链表中唤醒线程节点_EntryList = NULL ;_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;
}
3、ObjectMonitor 的内部抢锁过程
内部抢锁流程图如下:
步骤说明:
- JVM每次从队列的尾部取出一个数据用于锁竞争候选者( OnDeck ),但是并发情况下, ContentionList(contention:争论,争夺)会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到 EntryList (entry:进入)中作为候选竞争线程。
- Owner线程会在unlock时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList中的某个线程为 OnDeck线程 (一般是 最先进去 的那个线程)。
- Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“ 竞争切换 ”。
- OnDeck线程获取到锁资源后会变为 Owner线程 ,而没有得到锁资源的仍然停留在 EntryList 中。如果Owner线程被wait()方法阻塞,则转移到 Waiting Queue 中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去 EntryList 中。 处于 ContentionList 、 EntryList 、 WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
- Synchronized是非公平锁。 Synchronized在线程进入 ContentionList 前,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能 直接抢占 OnDeck线程的锁资源。
4、重量级锁的实现流程
6.2 内核态和用户态
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,线程的阻塞或者唤醒都需要操作系统来帮忙,Linux内核下采用pthread_mutex_lock系统调用实现,进程需要从用户态切换到内核态。
Linux系统的体系架构分为用户态(或者用户空间)和内核态(或者内核空间)。
1、内核态
Linux系统的内核是一组特殊的软件程序,负责控制计 图 2-15 Linux 进程的用户态与内核态算机的硬件资源,例如协调CPU资源、分配内存资源,并且提供稳定的环境供应用程序运行。
2、用户态
应用程序的活动空间为用户空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。
用户态是应用程序运行的空间,为了能访问到内核管理的资源(例如CPU、内存、I/O),可以通过内核态所提供的访问接口实现,这些接口就叫系统调用。
3、用户态内核态切换
用户态与内核态有各自专用的内存空间、专用的寄存器等,进程从用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
用户态的进程能够访问的资源受到了极大的控制,而运行在内核态的进程可以“为所欲为”。一个进程可以运行在用户态,也可以运行在内核态,那么它们之间肯定存在用户态和内核态切换的过程。进程从用户态到内核态切换主要包括以下三种方式:
- 硬件中断。硬件中断也称为外设中断,当外设完成用户请求时,会向CPU发送中断信号。
- 系统调用。其实系统调用本身就是中断,只不过是软件中断,与硬件中断不同。
- 异常。如果当前进程运行在用户态,这时发生了异常事件(例如缺页异常),就会触发切换。
4、pthread_mutex_lock系统调用
pthread_mutex_lock系统调用是内核态为用户态进程提供的Linux内核态下互斥锁的访问机制,所以使用pthread_mutex_lock系统调用时,进程需要从用户态切换到内核态,而这种切换是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
由于JVM轻量级锁使用CAS进行自旋抢锁,这些CAS操作都处于用户态下,进程不存在用户态和内核态之间的运行切换,因此JVM轻量级锁开销较小。而JVM重量级锁使用了Linux内核态下的互斥锁(Mutex),这是重量级锁开销很大的原因。
6.3 重量级锁的演示案例
重量级锁演示代码如下:
package com.crazymakercircle.innerlock;import com.crazymakercircle.util.Print;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;import java.util.concurrent.CountDownLatch;import static com.crazymakercircle.util.ThreadUtil.sleepMilliSeconds;/*** Created by 尼恩@疯狂创客圈.*/
public class InnerLockTest {final int MAX_TREAD = 10;final int MAX_TURN = 1000;CountDownLatch latch = new CountDownLatch(MAX_TREAD);@org.junit.Testpublic void showHeavyweightLock() throws InterruptedException {Print.tcfo(VM.current().details());//JVM延迟偏向锁sleepMilliSeconds(5000);ObjectLock counter = new ObjectLock();Print.tcfo("抢占锁前, counter 的状态: ");counter.printObjectStruct();sleepMilliSeconds(5000);CountDownLatch latch = new CountDownLatch(3);Runnable runnable = () ->{for (int i = 0; i < MAX_TURN; i++) {synchronized (counter) {counter.increase();if (i == 0) {Print.tcfo("第一个线程占有锁, counter 的状态: ");counter.printObjectStruct();}}}//循环完毕latch.countDown();//线程虽然释放锁,但是一直存在for (int j = 0; ; j++) {//每一次循环等待1mssleepMilliSeconds(1);}};new Thread(runnable).start();sleepMilliSeconds(1000); //等待2sRunnable lightweightRunnable = () ->{for (int i = 0; i < MAX_TURN; i++) {synchronized (counter) {counter.increase();if (i == 0) {Print.tcfo("占有锁, counter 的状态: ");counter.printObjectStruct();}//每一次循环等待10mssleepMilliSeconds(1);}}//循环完毕latch.countDown();};new Thread(lightweightRunnable, "抢锁线程1").start();sleepMilliSeconds(100); //等待2snew Thread(lightweightRunnable, "抢锁线程2").start();//等待加锁线程执行完成latch.await();sleepMilliSeconds(2000); //等待2sPrint.tcfo("释放锁后, counter 的状态: ");counter.printObjectStruct();}}
七、锁升级以及各种锁的对比
7.1 锁升级的实现流程
synchronized锁的升级流程如下:
synchronized执行过程如下:
- 线程抢锁时,JVM首先检测内置锁对象Mark Word中biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
- 在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
- 如果Mark Word中线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就将Mark Word中线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码,此时内置锁对象处于偏向锁状态。
- 如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
- JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
- 如果JVM的CAS替换锁记录指针自旋失败,轻量级锁膨胀为重量级锁,后面等待锁的线程也要进入阻塞状态。
总体来说,偏向锁是在没有发生锁争用的情况下使用;一旦有了第二个线程的争用锁,偏向锁就会升级为轻量级锁;如果锁争用很激烈,轻量级锁的CAS自旋到达阈值后,轻量级锁就会升级为重量级锁。
7.2 偏向锁、轻量级锁、重量级锁的对比
偏向锁、轻量级锁、重量级锁三种锁的对比如下:
八、Synchronized与Lock对比
8.1 synchronized的缺陷
1、效率低
锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
2、不够灵活
加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
3、无法知道是否成功获得锁
相对而言,Lock可以拿到锁状态,而synchronized不能获取到锁状态。
8.2 Lock解决相应问题
详情请看:[并发编程-AbstractQueuedSynchronizer (AQS) 核心原理及应用](#并发编程-AbstractQueuedSynchronizer (AQS) 核心原理及应用)
参考
https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
《极致经典(卷2):Java高并发核心编程(卷2 加强版)》 作者:尼恩
相关文章:

并发编程-synchronized解决原子性问题
并发编程-synchronized解决原子性问题 文章目录 并发编程-synchronized解决原子性问题零、说在前面一、线程安全问题1.1 什么是线程安全问题1.2 自增运算不是线程安全的1.3 临界区资源与临界区代码段 二、synchronized 关键字的使用2.1 synchronized 关键字作用2.2 synchronize…...

CSS之我不会
非常推荐html-css学习视频:尚硅谷html-css 一、选择器 作用:选择页面上的某一个后者某一类元素 基本选择器 1.标签选择器 格式:标签{} <h1>666</h1><style>h1{css语法} </style>2.类选择器 格式:.类…...

AI绘画:SD打光神器!(Stable Diffusion进阶篇:Imposing Consistent Light)
前言 在上一篇笔记中学习了如何简单地下载以及使用IC-Light,今天的内容会稍微有点不一样。 对于学过stable diffusion的小伙伴来说,forge UI和Comfy UI会更加熟悉一些。在IC-Light发布后,Openpose editor的开发者将其制作成了一个Forge UI上…...

QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期]
QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期] 第二期介绍:频道模块之频道管理 目录 QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期]第二期介绍:频道模块之频道管理获取用户详情获取用户频道列表获取频道详情获取子频道列表获…...

参赛心得和思路分享:2021第二届云原生编程挑战赛2: 实现一个柔性集群调度机制
关联比赛: 2021第二届云原生编程挑战赛2:实现一个柔性集群调度机制 参赛心得 历时快两个月的第二届云原生编程挑战赛结束了,作为第一次参赛的萌新,拿下了28名的成绩,与第一名差了19万分,因为赛制时间太长,…...

具体函数的卡诺图填入
目录 用卡诺图表示逻辑函数 基本步骤 例子1 例子2 例子3 用卡诺图表示逻辑函数 基本步骤 例子1 由真值表得卡诺图。 在函数值为1的地方在卡诺图上画上1。 例子2 例子3 非标准与或式,要找到公共部分。 将AB所在的那一行填上1。 将A非D的那个部分也填上1。 再…...

STM32 HAL freertos零基础(六)计数型信号量
1、计数型信号量 计数型信号量(Counting Semaphore)是另一种类型的信号量,它可以保持一个大于等于0的整数值,这个值表示可用资源的数量。本质上相当于队列长度大于1得队列。经典问题就是剩余车辆统计,出入车辆,车辆数据可以实时更新。 2、相关API函数 xSemaphoreCreat…...

Dynamics CRM Ribbon Workbench-the solution contains non-entity components
今天在一个低版本的环境里准备用Ribbon Workbench去编辑一个按钮时,遇到了如下错误 一开始没当回事,以为是我的解决方案问题,去检查了下,只有一个组件,并且哪怕我把组件换成了某个实体也不行,尝试了其他任何…...

谷歌对抗司法部:为什么谷歌的“数百个竞争对手”说法站不住脚
随着谷歌反垄断陪审团审判的进行,谷歌声称美国司法部对广告技术市场的看法狭隘,并且广告商和出版商有很多替代选择。然而,证据并不支持这一说法。 谷歌误导性地声称有“数百个竞争对手。” 虽然存在许多广告技术提供商,但谷歌在…...

重生奇迹MU 沉迷升级的快感 法魔升级机器人
重生奇迹MU是一款以升级为主要玩法的游戏。升级是游戏基础,也是最重要的部分。通过升级,玩家可以获得更多的基础属性奖励和自由点数奖励,同时还能够穿戴最好的装备和翅膀。因此,升级在游戏中具有极其重要的地位。 史上升级最快的…...

从地图到智能地图:空间索引技术如何改变我们的世界
图源:WL 为什么空间索引很有用? 在处理地理空间数据时,空间索引是一个至关重要的功能,它决定了我们如何高效地从海量的地理数据中检索出所需的信息。想象一下,如果你正在处理一个包含数千万乃至数亿条记录的数据库,…...

深入理解AI Agent架构,史上最全解析!赶紧码住!
AI Agent框架(LLM Agent):LLM驱动的智能体如何引领行业变革,应用探索与未来展望 1. AI Agent(LLM Agent)介绍 1.1. 术语 Agent:“代理” 通常是指有意行动的表现。在哲学领域,Agen…...

苹果iOS/ iPadOS18 RC 版、17.7 RC版更新发布
iPhone 16 / Pro 系列新机发布后,苹果一同推出了 iOS 18 和 iPadOS 18 的 RC 版本,iOS 18 RC 的内部版本号为22A3354,本次更新距离上次发布 Beta/RC 间隔 12 天。 在 iOS 18 中,苹果给我们带来了 Apple Intelligence,这…...

CAN集线器(工业级、隔离式)
型号: MS-HUB-C 概述 MS-HUB 是一款可通过一路 CAN ,一路 RS-232为主口扩展出 7 路 CAN 从口的工业级光电隔离型 CAN 分配器。可以有效的实现 CAN 网络的中继、扩展与隔离。采用先进的自动流控技术自动侦测CAN 信号流向。MS-HUB 具备光电隔离功能&#x…...

代码随想录训练营 Day57打卡 图论part07 53. 寻宝(prim,kruskal算法)
代码随想录训练营 Day57打卡 图论part07 卡码53. 寻宝 题目描述 在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 不同岛屿之间,路途距离不同,…...

Hibernate QueryPlanCache 查询计划缓存引发的内存溢出
目录 1.排查方式2.结论3.解决办法 前言:在生产环境中有一个后端程序多次报oom然后导致程序中断。 1.排查方式 通过下载后端程序产生的oom文件,将oom文件导入MemoryAnalyzer程序分析程序堆内存使用情况。 1、将oom文件导入MemoryAnalyzer后可以看到概览信…...

前端开发的观察者模式
什么是观察者设计模式 观察者模式(Observer Pattern)是前端开发中常用的一种设计模式。它定义了一种一对多的依赖关系,使得当一个对象的状态发生改变时,其所有依赖对象都能收到通知并自动更新。观察者模式广泛应用于事件驱动的系…...

Pycharm 输入三个引号没有自动生成函数(方法)注释
配置项路径:pycharm–>Settins–>Tools–>Python Integrated Tools–>Docstrings–>Docstrings format选择对应的工程,如果有多个工程的话将 Docstrings format 的值从 Plain 换成 reStructuredText...

lammps后处理:多帧孔洞体积和孔隙率的计算
本文介绍lammps后处理技巧:多帧孔洞体积和孔隙率的计算方法。 在前面的专栏中,已经介绍了单帧孔洞体积的计算方法,有不少粉丝朋友咨询多帧孔洞体积的计算方法。 在上一次案例代码的基础上,稍加修改,添加一个for循环遍历所有的帧即可实现多帧孔洞体积的计算。 计算的结果…...

免费且实用:UI设计常用的颜色参考网站和一些Icon设计网站
用心去分享!请给我点个关注和点赞收藏!谢谢各位努力的人才! 1.在UI设计的时候,没有灵感,怎么办?可以参考这个网站(需要魔法能量) 网址如下: Color Hunt - Color Palette…...

log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈
日志是项目中必用的东西,日志产品里最普及应该就是log4j了。(logback这里暂不讨论。) 先看一下常用的log4j的用法,一般来说log4j都会配合slf4j或者common-logging使用,这里已slf4j为例。添加gradle依赖: dependencies { compile(l…...

JVM面试真题总结(六)
文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 解释GC的标记-整理算法及其优点 GC(垃圾收集ÿ…...

C语言代码练习(第十八天)
今日练习: 48、猴子吃桃问题。猴子第1天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第2天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时&…...

linux上使用rpm的方式安装mysql
1.从mysql官网上下载需要的版本,根据操作系统版本,CPU架构,下载让rpm bundle,这个版本是个完整版,包含其他所有版本 上传到服务器的一个目录,进行解压 执行tar -xvf mysql*.tar tar -xvf mysql*.tar 2.卸载老版本m…...

html 中如何使用 uniapp 的部分方法
示例代码: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><…...

Samtec连接器小课堂 | 连接器电镀常识QA
【摘要/前言】 像大多数电子元件一样,无数子元件和工艺的质量直接影响到成品的质量和性能。对于PCB级连接器,这些因素包括针脚材料、塑料类型、模制塑料体的质量、尾部的共面性、表面处理(电镀)的质量、选择正确的连接器电镀、制…...

大模型备案全网最详细流程解读(附附件+重点解读)
文章目录 一、语料安全评估 二、黑盒测试 三、模型安全措施评估 四、性能评估 五、性能评估 六、安全性评估 七、可解释性评估 八、法律和合规性评估 九、应急管理措施 十、材料准备 十一、【线下流程】大模型备案线下详细步骤说明 十二、【线上流程】算法备案填报…...

基于2143规则编码的uint8_t如何转换成float
2143格式存储的float类型数据在解码时,需要将1和2互换,3和4互换,然后 通过数组转float,进行转换 uint8_t data[] {0x72, 0x02, 0xc8, 0x42}; // 字节数组 float bytesToFloat(uint8_t data[]) { uint32_t x; memcpy(&x, da…...

[项目][WebServer][整体框架设计]详细讲解
目录 0.框架 && 前言1.TcpServer类1.功能2.类设计 2.HttpServer类1.功能2.类设计 3.Request类 && Response类1.功能2.Request类设计3.Response类设计 4.EndPoint类1.功能2.类设计 5.Task类1.功能2.类设计 6.ThreadPool类1.功能2.类设计 0.框架 && 前言…...

SprinBoot+Vue网上购物商城的设计与实现
目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍:CSDN认证博客专家,CSDN平台Java领域优质…...