> 文档中心 > 【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

【衔接上一章 【并发编程八:线程安全问题分析及锁的介绍(1)】】

2.5.3对象头Mark Word中VALUE值解读

Intel、AMD处理器(CPU)是小端存储;
以字节为单位,从右到左是由高到低,字节内部是从左到右为由高到低;
我们日常:1234 小端:4321

  • 32位虚拟机Mark Word为4字节;
  • 64位虚拟机Mark Word为8字节;

2.5.4偏向锁

前面我们介绍了synchronized锁的类型分为:偏向锁、轻量级锁、重量级锁;**
JDK1.6之前,synchronized只有重量级锁,从JDK1.6开始新增了偏向锁、轻量级锁;

  • 在没有多线程竞争时,访问synchronized修饰的同步代码,会先使用偏向锁;
  • 比如在实际应用场景中,使用synchronized修饰同步代码是为了防止出现线程安全问题,但是实际运行时我们不知道会不会有多线程去访问,而且实际上大部分时间都没有多线程去访问,所以我们完全没有必要一开始就采用重量级锁,当没有多线程访问时候,synchronized此时使用的是偏向锁,这是- JDK对synchronized的优化;
  • JVM启动参数 -XX:BiasedLockingStartupDelay默认是4秒,可以改成0;
    打印所有JVM参数及默认值:java -XX:+PrintFlagsFinal -version

3.5.4.1偏向锁加锁

抢占偏向锁的过程是通过CAS(Compare and Swap)实现的,通过CAS替换Mark Word中的偏向锁标记和锁标记,并设置线程ID;
【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

2.5.4.2偏向锁释放

偏向锁不好明确地说释放锁,可以认为偏向锁是没有释放锁这个概念的,也就是说偏向锁不会主动去释放锁,偏向锁是等到有另外的线程来获取锁,出现锁竞争才有释放锁的概念,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,然后发生锁的升级/膨胀;

2.5.4.3偏向锁的设计思想

/** * 偏向锁 * */public class TestBiasedLock {    private int id = 100;    private int uid = 1001;    private long no = 10893230588L;    public static void main(String[] args) throws InterruptedException { Thread.sleep(6000); TestBiasedLock t = new TestBiasedLock(); System.out.println("------加锁前------"); String classLayout = ClassLayout.parseInstance(t).toPrintable(); System.out.println(classLayout); //加锁 synchronized (t) {     System.out.println("------加锁后------");     String classLayoutAfter = ClassLayout.parseInstance(t).toPrintable();     System.out.println(classLayoutAfter); }    }}

·· //解锁

  • 偏向的意思:偏心、偏袒,也就是每次都对某一个线程关爱有加、享受高级特殊待遇;
  • 在没有线程竞争的情况下,每次都是同一个线程访问同步代码块,那就没有必要加锁和解锁,只需要第一次设置对象头标记,后续只需要判断一下对象头即可,提升性能;
  • 如果此时有其他线程加入了竞争,那就会进行锁的升级/膨胀;
  • 偏向锁是假定每次来访问同步代码块都是同一个线程,没有其他线程来竞争锁;
  • 如果你明显觉得项目中加锁的代码大部分情况都会是多线程竞争锁,此时完全可以把偏向锁关闭,-XX:-UseBiasedLocking,那么程序默认会进入轻量级锁状态;

2.5.5轻量级锁

  • 偏向锁是同一时刻只有一个线程获取锁,如果有多个线程来获取锁,比如说有几个线程交替执行同步代码块,此时如果直接使用重量级锁,可能也有点过头了,就会产生更多的性能消耗,因为重量级锁需要使用操作系统互斥锁来实现,需要从用户态到内核态的切换,于是JDK设计了一个平衡方案那就是轻量级锁;
  • 轻量级锁就是没有抢占到锁的线程,进行一定次数的重试,也叫自旋(忙循环 while (…)),当然如果无限制地自旋,会消耗CPU资源,JDK6默认是10次,可以使用-XX:PreBlockSpin来更改;
    同时JDK对自旋做了进一步改进,引入了自适应自旋锁,即自旋的次数是不固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间来获取锁,如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费CPU资源;
public class TestLightWeightLock {    private int id = 100;    private int uid = 1001;    private long no = 10893230588L;    public static void main(String[] args) throws InterruptedException { TestLightWeightLock t = new TestLightWeightLock(); System.out.println("------加锁前------"); String classLayout = ClassLayout.parseInstance(t).toPrintable(); System.out.println(classLayout); //加锁 synchronized (t) {     System.out.println("------加锁后------");     String classLayoutAfter = ClassLayout.parseInstance(t).toPrintable();     System.out.println(classLayoutAfter);     System.out.println("------再次加锁后------");     synchronized (t) {  String classLayoutAgain = ClassLayout.parseInstance(t).toPrintable();  System.out.println(classLayoutAgain);     } } //synchronized释放锁了 System.out.println("------synchronized执行完了,释放锁后------"); String classLayoutFinal = ClassLayout.parseInstance(t).toPrintable(); System.out.println(classLayoutFinal);    }}

3.5.5.1轻量级锁加锁

  • 1、在线程进入同步块时,如果同步对象锁状态为无锁状态(偏向锁为0,锁标志位为01),虚拟机首先将在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象目前的Mark Word拷贝,官方称之为 Displaced Mark Word,此时线程栈与对象头的状态如图:
    【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

  • 2、复制对象头中的Mark Word到锁记录(Lock Record)中,如下图所示:
    【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

  • 3、复制完成后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向Object mark word,如下图所示:
    【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

  • 4、如果上面的步骤3更新成功了,那么这个线程就拥有了该对象的锁,并将对象Mark Word的锁标记设置为00,即表示此对象处于轻量级锁定状态,如下图所示:
    【并发编程九:线程安全问题分析及锁的介绍(2)synchronized】

  • 5、如果上面的步骤3更新失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁,并且竞争很激烈,如果不激烈的话,当前线程不需要阻塞,进行自旋,等待占用锁的线程释放锁,通过CAS后即可立即获取锁,否则轻量级锁就要升级为重量级锁;(如果竞争不激烈的话,上面的步骤3应该是可以获取锁的);

3.5.5.2轻量级锁释放

  • 轻量级锁释放锁(解锁)时,会使用CAS将之前复制在栈桢中的 Displaced Mard Word 替换回 Mark Word 中,如果替换成功,则说明整个过程都成功执行,期间没有其他线程访问同步代码块;
  • 但如果替换失败了,表示当前线程在执行同步代码块期间,有其他线程也在访问,当前锁资源是存在竞争的,那么锁将会膨胀成重量级锁;

3.5.5.3轻量级锁的设计思想

  • 轻量级锁是相对重量级锁而言的(重量级锁基于操作系统的互斥量实现),轻量级锁的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统的互斥量而带来的性能消耗;
  • JDK的设计人员根据大量数据和经验表明:对于绝大部分锁,在整个同步周期内都是不存在竞争的,如果没有竞争,轻量级锁就可以使用 CAS 操作避免采用操作系统互斥量获取锁的开销,从而提升效率;
  • 偏向锁 --> 轻量级锁 --> 重量级锁;

3.5.6重量级锁

  • JDK1.6之前,synchronized只有重量级锁,JDK1.6及之后的版本,
  • -synchronized锁从偏向锁–>轻量级锁–>重量级锁,当升级为重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程时,都需要操作系统来调度,这就需要从用户态转换到内核态,这种转换是需要消耗很多时间的,所以这种重量级锁同步方式的成本非常高;
import org.openjdk.jol.info.ClassLayout;import java.util.concurrent.TimeUnit;public class TestHeavyWeightLock {    public static void main(String[] args) throws InterruptedException { TestHeavyWeightLock t = new TestHeavyWeightLock(); System.out.println("------加锁前------"); String classLayout = ClassLayout.parseInstance(t).toPrintable(); System.out.println(classLayout); //创建一个线程 Thread t1 = new Thread(() -> {     synchronized (t) {  try {      TimeUnit.MILLISECONDS.sleep(3000);  } catch (InterruptedException e) {      e.printStackTrace();  }  System.out.println("t1线程执行......");     } }); t1.start(); //main线程休眠500毫秒,确保t1线程已经开始执行 TimeUnit.MILLISECONDS.sleep(500); System.out.println("t1线程已经抢占到了t对象的锁,输出t对象的内存布局:"); String syncClassLayout = ClassLayout.parseInstance(t).toPrintable(); System.out.println(syncClassLayout); //main线程抢占t对象的锁 synchronized (t) {     System.out.println("------main线程抢占到了t对象的锁------");     String classLayoutAfter = ClassLayout.parseInstance(t).toPrintable();     System.out.println(classLayoutAfter); }    }}

驱动天空下载