绝味鸭脖网站建设规划书,网站设计的主要风格,经营虚拟网站策划书,把网站做静态化并发编程-synchronized解决原子性问题 文章目录 并发编程-synchronized解决原子性问题零、说在前面一、线程安全问题1.1 什么是线程安全问题1.2 自增运算不是线程安全的1.3 临界区资源与临界区代码段 二、synchronized 关键字的使用2.1 synchronized 关键字作用2.2 synchronize…并发编程-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修饰普通方法锁对象默认为this3、类锁-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、监视器ObjectMonitor3、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?inviteCodeXT16BKSO3S 先看看美景养养眼再继续以下乏味的学习内容有点多建议收藏分多次食用。
一、线程安全问题
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内部是独立进行的中间完全可能会出现多个线程并发进行。
比如在amount100时假设有三个线程同一时间读取amount值读到的都是100增加1后结果为101三个线程都将结果存入到amount的内存amount的结果是101而不是103。
“内存取值”“寄存器增加1”“存值到内存”这三个JVM指令是不可以再分的它们都具备原子性是线程安全的也叫原子操作。但是两个或者两个以上的原子操作合在一起进行操作就不再具备原子性。比如先读后写就有可能在读之后其实这个变量被修改了就出现了数据不一致的情况。
1.3 临界区资源与临界区代码段
临界区资源表示一种可以被多个线程使用的公共资源或共享数据但是每一次只能有一个线程使用它。一旦临界区资源被占用想使用该资源的其他线程则必须等待。
在并发情况下临界区资源是受保护的对象。临界区代码段Critical Section是每个线程中访问临界资源的那段代码多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前必须在进入区申请资源申请成功之后进行临界区代码段执行完成之后释放资源。 二、synchronized 关键字的使用
2.1 synchronized 关键字作用
在Java中线程同步使用最多的方法是使用synchronized关键字。每个Java对象都隐含有一把锁这里称为Java内置锁或者对象锁、隐式锁。使用synchronizedsyncObject调用相当于获取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 但是并发情况下 ContentionListcontention争论争夺会被大量的并发线程进行 CAS 访问为了降低对尾部元素的竞争JVM会将一部分线程移动到 EntryList entry进入中作为候选竞争线程。Owner线程会在unlock时将 ContentionList 中的部分线程迁移到 EntryList 中并指定EntryList中的某个线程为 OnDeck线程 一般是 最先进去 的那个线程。Owner线程并不直接把锁传递给OnDeck线程而是把锁竞争的权利交给OnDeckOnDeck需要重新竞争锁。这样虽然牺牲了一些公平性但是能极大的提升系统的吞吐量在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偏向锁标识是否设置成1lock锁标志位是否为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 《极致经典卷2Java高并发核心编程(卷2 加强版)》 作者尼恩