02 Java的synchronized原理与Monitor对象

1 基本概念

案例:采用2个无关联的线程对同一变量进行操作,一个进行5000次自增操作,另外一个进行5000次自减操作。

最终变量的结果是不确定的(2个算数操作的操作指令由于多线程原因会交错在一起)。

临界区(critical section):对共享资源进行多线程读写操作的代码块。

竞态条件(Race Condition):多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称之为发生了竞态条件

Java中如何避免发生竞态条件?

  • 阻塞式解决方案:synchronized, Lock
    • synchronized俗称“对象锁”,采用互斥的方式使得同一时刻最多只有一个线程能拥有这个“对象锁”。
  • 非阻塞式:原子变量

2 Java中synchronized的使用与理解

synchronized (对象){         // 申请对象锁
           临界区;
}

2-1 基本的使用

package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test1 {
    static int counter = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 5000;++i){
                synchronized (lock){        // 申请对象锁
                    counter++;
                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for(int i = 0;i < 5000;++i){
                synchronized (lock){       // 申请对象锁
                    counter--;
                }
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.warn("{}",counter);        // 通过synchronized实现了对共享变量的互斥操作
    }
}

结果

[main] WARN c.Test1 - 0

总结:Java中使用synchronized以对象锁的形式保证了临界区的原子性,避免竞态条件的发生。

上面代码引申:

  • 代码中2次synchronized必须是同一对象

  • 代码中仅仅进行一次synchronized无法保证竞态条件不发生。

对共享变量进行封装:

package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test2 {
    static Room room = new Room();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 5000;++i){
                synchronized (room){        // 申请对象锁
                    room.increment();
                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            for(int i = 0;i < 5000;++i){
                synchronized (room){       // 申请对象锁
                    room.decrement();
                }
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.warn("{}",room.getCounter());   // 通过synchronized实现了对共享变量的互斥操作
    }
}

class Room{
    private static int counter = 0;
    public void increment(){
        synchronized (this){
            counter++;
        }
    }

    public void decrement(){
        synchronized (this){
            counter--;
        }
    }

    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}

2-2 方法上的synchronized

2种等价写法:

class Test{
      public void test(){
          synchronized (this){     // this表示当前类的实例,也叫做qualified this
              counter++;
          }
      }
}
等价于
class Test{
      public synchronized void test(){
          counter++;
      }
}
  
//静态方法
class Test{
      public static void test(){
          synchronized (Test.class){
              counter++;
          }
      }
}
等价于
class Test{
      public synchronized static void test(){
          counter++;
      }
}

静态方法的synchronized与普通成员方法synchronized的区别:

  • 静态方法上锁的是这个class。
  • 普通成员方法,锁的是该对象的实例this。
  • 一个class可以多个this实例

2-3 变量的线程安全分析

线程安全:多个线程执行同一段代码,所得到的最终结果是否符合预期。

局部变量:
  • 局部变量是线程安全的
    • 实例:栈帧中每一个frame存储的变量都是相互独立的。
  • 局部变量引用的对象未必:
    • 线程安全的判断依旧:引用的对象是否脱离方法的作用范围
静态变量:
  • 静态变量没有被多个线程共享,或者被多个线程共享但只进行读操作,那么该静态变量就是线程安全的。
实例1:局部变量引用带来的线程不安全
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@Slf4j(topic = "c.Test4")
public class Test4 {
    static final int LOOP_NUMBER = 200;
    static final int THREAD_NUMBER = 2;
    public static void main(String[] args){
        ThreadUnsafeExample tmp = new ThreadUnsafeExample();
        for(int i = 0;i < THREAD_NUMBER;++i){
                new Thread(()->{
                    tmp.method1(LOOP_NUMBER);
                },"Thread"+i).start();
        }
    }
}

// 这里定义了一个线程不安全的类
class ThreadUnsafeExample{
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopnumber){
        for(int i = 0;i < loopnumber;++i){
            method2();
            method3();
        }
    }
    private void method2(){
        list.add("1");
    }

    private void method3(){
        list.remove(0);
    }

}

运行结果:

Exception in thread "Thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
	at java.util.ArrayList.remove(ArrayList.java:492)
	at chapter3.ThreadUnsafeExample.method3(Test4.java:35)
	at chapter3.ThreadUnsafeExample.method1(Test4.java:27)
	at chapter3.Test4.lambda$main$0(Test4.java:15)
	at java.lang.Thread.run(Thread.java:748)

分析:

  • 多个线程通过成员变量共享了堆中的list对象。
    02 Java的synchronized原理与Monitor对象
实例2:局部变量的引用暴露带来的线程不安全
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;

@Slf4j(topic = "c.Test5")
public class Test5 {
    static final int LOOP_NUMBER = 10000;
    static final int THREAD_NUMBER = 2;
    public static void main(String[] args){
        ThreadSafeExampelSubclass tmp = new ThreadSafeExampelSubclass();
        for(int i = 0;i < THREAD_NUMBER;++i){
                new Thread(()->{
                    tmp.method1(LOOP_NUMBER);
                },"Thread"+i).start();
        }
    }
}

class ThreadsafeExample{
    public void method1(int loopnumber){
        ArrayList<String> list = new ArrayList<>();    //方法中new了一个对象,每个线程调用该方法都会new一个对象,因此不存在线程之间共享的成员,所以是安全的。
        for(int i = 0;i < loopnumber;++i){
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list){
        list.add("1");
    }
    public void method3(ArrayList<String> list){
        list.remove(0);
    }
}

class ThreadSafeExampelSubclass extends ThreadsafeExample{
    @Override
    public void method3(ArrayList<String> list){     // 方法内部创建的对象的引用通过继承被暴露了
        new Thread(()->{
            list.remove(0);
        }).start();
    }
}

运行结果会出现2种:

  • 没有任何问题,程序正常退出(循环次数比较小的情况下)
  • 出现如下错误:
Exception in thread "Thread-1446" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
	at java.util.ArrayList.remove(ArrayList.java:492)
	at chapter3.ThreadSafeExampelSubclass.lambda$method3$0(Test5.java:41)
	at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0

分析:由于父类使用public修饰list的操作方法,因此对于list的引用被暴露给子类。

  • 子类通过重载将局部变量引用的对象被多个线程共享,引发问题

线程安全问题实际挺难发现的可以通过一些良好的编程习惯避免。

通过private,final等关键词保证安全,遵循面向对象编程的开闭原则的闭。

class ThreadsafeExample{
    public final void method(int loopnumber){
        ArrayList<String> list = new ArrayList<>();
        for(int i = 0;i < loopnumber;++i){
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list){
        list.add("1");
    }
    private void method3(ArrayList<String> list){
        list.remove(0);
    }
}

2-4 常用的线程安全类

基本理解

Java中常用的线程安全类:String, Integer, StringBuffer, Random, Vector, HashTable, java.util.concurrent(juc包)

线程安全类的理解:多个线程调用同一实例的某个方法时,是线程安全的。

  • 可以理解为线程安全的类的方法是原子的(查看源码会发现有synchronized)
  • 注意多个方法的组合未必是原子的。
HashTable table = new HashTable();
//线程1,线程2都会执行的代码
if(table.get("key") == null){
	table.put("key",value);
}

分析: 虽然HashTable是线程安全的,但是上面的代码并不是线程安全的,在实际调度时可以出现下面的情况:

线程1.get   -->  线程2.get  --> 线程1.put  ---> 线程2.put

即无法保证同一线程中get与put同时执行。想要保证可以另外synchronized。

不可变类的线程安全

包括:String, Integer

由于不可变性,所以这个类别是线程安全的。

String中replace,substring方法如何保证线程安全?

这些方法并没有改变原有的字符串,而是直接创建了一个新的字符串。

实例:String中substring源码(最后return是一个新的实例)

    public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

2-5 线程安全分析实例(重点)

案例1

public class MyServlet extends HttpServlet {
    // 是否安全?  不是线程安全的,可以用线程安全的类HashMap去替代。
    Map<String,Object> map = new HashMap<>();
    // 是否安全?  线程安全,String是不可变类
    String S1 = "...";
    // 是否安全?  线程安全,String是不可变类
    final String S2 = "...";
    // 是否安全?  线程不安全,Data()不是线程安全类,其成员可能会引发安全问题。
    Date D1 = new Date();
    // 是否安全?  线程不安全,利用同上 
    final Date D2 = new Date();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    // 使用上述变量
	}
}

Servlet是运行在tomcat环境下,只有一个实例,所以servlet必定会被tomcat多个线程共享使用

重点:分析成员变量在多线程环境下的安全性。


案例2

public class MyServlet extends HttpServlet {
    // 是否安全?  不是线程安全的,成员变量count并不安全,UserServiceImpl实例受Servlet限制一般也只有
    // 一个。
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
	}
}

public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;
    public void update() {
        // ...
        count++;
    }
}

案例3

这里利用AOP监测程序运行时间,可以采用环绕通知保证线程安全。

@Aspect
@Component
public class MyAspect {
	// 是否安全? 不是线程安全的,变量start可以被同一实例的多个线程调用共享
    private long start = 0L; 
    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }
    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}

案例4

public class MyServlet extends HttpServlet {
	// 是否安全 是线程安全的
    private UserService userService = new UserServiceImpl();
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
	}
}

public class UserServiceImpl implements UserService {
	// 是否安全  是线程安全的,没有对成员的修改操作
	private UserDao userDao = new UserDaoImpl();
	public void update() {
		userDao.update();
	}
}

public class UserDaoImpl implements UserDao {
	public void update() {
        String sql = "update user set password = ? where username = ?";
            // 是否安全 是线程安全,每个新的线程都会新建一个connection
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
	}
}

案例5

public class MyServlet extends HttpServlet {
    // 是否安全   不安全
    private UserService userService = new UserServiceImpl();
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
	}
}

public class UserServiceImpl implements UserService {
    // 是否安全   不安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全 , 不是线程安全的,成员变量conn不安全。被多个线程共享
    // 需要将conn设为局部变量
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
	}
}

案例6

public class MyServlet extends HttpServlet {
    // 是否安全  安全
    // UserDao userDao = new UserDaoImpl();确保了线程安全,每有一个新的链接都重新new一个,
    // 但是这种写法不推荐,浪费资源。
	private UserService userService = new UserServiceImpl();
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
	}
}

public class UserServiceImpl implements UserService {
	public void update() {
		UserDao userDao = new UserDaoImpl();
		userDao.update();
	}
}

public class UserDaoImpl implements UserDao {
    // 是否安全  不是线程安全的,成员变量conn不安全。可以被同一实例多个线程共享
    private Connection = null;
	public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
		// ...
		conn.close();
	}
}

案例7:判断对象的引用是否泄露,警惕抽象方法引入的外星方法。

// 定义了一个抽象类
public abstract class Test {
    public void bar() {
        // 是否安全     
        // 不安全,
        // 子类对foo方法定义并在foo中启动新的线程访问sdf对象,造成sdf在多个线程中出现共享,sdf并不是
        // 这个案例与之前引用暴露带来的不安全问题如出一辙。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    public abstract foo(SimpleDateFormat sdf);
    public static void main(String[] args) {
        new Test().bar();
    }
}
// 
public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
            	sdf.parse(dateStr);
            } catch (ParseException e) {
            	e.printStackTrace();
            }
        }).start();
    }
}

foo在子类中的定义(这里对变量进行了修改)

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
	for (int i = 0; i < 20; i++) {
        new Thread(() -> {
		try {
            sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
        	}
        }).start();
	}
}

案例8: String的源代码中对String类定义为何加上final这个关键词?

Final用于修饰类、成员变量和成员方法。final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);Final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;Final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。
  • final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类)
  • 避免用户定义的String中的子类破坏其原有方法的安全性。
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

2-6 多线程卖票实例分析

错误并行代码:

package chapter3.exericse;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;


@Slf4j(topic = "c.Ticket")
public class Ticket {
    public static void main(String[] args) throws InterruptedException {
        TicketWindow ticketWindow  = new TicketWindow(10000);
        List<Thread> threadList = new ArrayList<>();  // 用于同步所有线程,让所有线程都结束
        List<Integer> amountList = new Vector<>();   // Vector是线程安全的,可以在多线程环境使用
        for(int i = 0;i < 1000; ++i){
            Thread thread = new Thread(()->{
                // 加个随机睡眠,确保出现问题
                try {
                    Thread.sleep(randomAmount());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int tmp = ticketWindow.sell(randomAmount());
                amountList.add(tmp);


            });
            threadList.add(thread);
            thread.start();
        }
        // 等待所有线程运行完毕
        for (Thread thread : threadList) {
            thread.join();
        }
        // 统计余票
        log.warn("余票:{}",ticketWindow.getCount());
        //统计实际卖出的票,求和
        log.warn("卖出的票: {}",amountList.stream().mapToInt(i->i).sum());
    }
    // random是线程安全的
    static Random random = new Random();
    public static int randomAmount(){
        //随机范围1-5
        return random.nextInt(5)+1;
    }
}

// 定义售票窗口,提供查看余票并售票的功能
// 这个类会在多线程环境下运行
class TicketWindow{
    // 统计剩余的票数
    private int count;
    public TicketWindow(int count){
        this.count = count;
    }

    public int getCount(){
        return count;
    }

    // 售票方法,返回售出票的数量
    public int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }else{
            return 0;
        }
    }
}

运行结果:

[main] WARN c.Ticket - 余票:7033
[main] WARN c.Ticket - 卖出的票: 2983

总结:

  • 可以看到定义的TicketWindow在多线程环境下出现票数的统计错误。说明这个类是线程不安全的。
  • 多线程问题难以复现:实际运行时发现多次运行有时候票数统计是正确的,有时候不正确,说明多线程问题比较难以排查。

买票的多线程问题分析:

可以发现多线程共享的成员有TicketWindow以及Vector对象的实例,Vector用到了add方法的,由于本身就是线程安全类,因此相关部分没有线程安全问题。

​ 而TicketWindow的sell方法中count在多线程环境下会被修改,相关联的代码就是临界区。因此可以加一个对象锁。修改代码如下所示。

    // 售票方法,返回售出票的数量
    public synchronized int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }else{
            return 0;
        }
    }

2-7 Monitor对象头以及synchronized工作原理(重要)

Java对象头的概念(32虚拟机情况):

  • 普通对象:object header由mark word和Klass word,Kclass word是一个指针,指向对象所从属的class。
    • mark word中存储了对象丰富的信息,注意mark word有5种状态表示,当给对象加上synchronized后,如果state是Heavyweight locked,此时加锁的对象通过mark word关联monitor对象。

02 Java的synchronized原理与Monitor对象

02 Java的synchronized原理与Monitor对象

  • 数组对象:对象头除了包含mark word以及Kclass word还有数组长度

实例:32位虚拟机下,int类型只占用4字节,而Integer占用4+8字节,其中8字节是对象头

Monitor(管程)的基本概念:

  • 管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
  • Java中的monitor:每个Java对象都可以关联一个monitor对象,如果对一个对象使用synchronized关键字,那个这个对象的对象头的mark word就被设置指向monitor对象的指针。

02 Java的synchronized原理与Monitor对象

上图中原理讲解:

  • 线程2执行synchronized(obj)会检查关联到Monitor对象中的owner为null,将owner设置为自己,每一个Monitor对象只能有一个owner。
  • 线程1和线程3执行到临界区代码后,同样检查Monitor对象中的owner,由于Monitor对象存在owner,所以进入Entrylist (阻塞队列)进行等待。

02 Java的synchronized原理与Monitor对象

Synchronized字节码层面理解

02 Java的synchronized原理与Monitor对象

02 Java的synchronized原理与Monitor对象

02 Java的synchronized原理与Monitor对象

总结:

  • 字节码第5行(monitor enter)就是代码执行到synchronized那里,然后将对象头中的mark word设置为Monitor指针
  • 字节码第11行(monitor exit)就是临界区代码执行完成,将对象头的的mark work重置,同时唤醒monitor对象中的EntryList,让其他线程进入临界区。
  • 19-23行适用于处理临界区代码出现异常的情况。

2-8 synchronized进阶工作原理

Monitor(重量级锁)虽然能够解决不安全问题,但代价有点高(需要为加锁对象关联一个monitor对象),为了降低代价引入了下列机制:

基本概念:

  • 轻量级锁
  • 偏向锁
    • 批量重刻名:一个类的偏向锁撤销达到20
    • 不可偏向:某个类别被撤销的次数达到一定阀值(代价过高),设置为不可偏向。

轻量级锁

  • 基本思想:利用线程中栈内存的锁记录结构作为轻量级锁,锁记录中存储锁定对象的mark word

  • 使用场景:对象虽然有多线程访问,但多线程加锁的时间是错开的(没有竞争)

  • 注意点:轻量级锁不需要用户指定,其使用是透明的,使用synchronized关键字。程序优先尝试轻量级锁。

2-8-1 轻量级锁的加锁过程
static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
    // 同步块 A
    method2();
    }
    }
    public static void method2() {
    synchronized( obj ) {
    // 同步块 B
    }
}

上面的代码中进行了2次加锁。

02 Java的synchronized原理与Monitor对象

step1:线程0首先在栈帧中创建锁记录对象

  • 锁记录的Object reference指向加锁的对象

step2: 使用CAS(Compare and Swap)操作替换加锁对象中对象头的mark word,将mark word存储到所记录

  • 替换成功,则加锁对象的mark word的锁记录地址和状态 00 ,表示light weight locked
  • 替换失败,有2种情况;
    • 一种是线程0以外的其他线程拥有这个线程的轻量锁,发生了竞争,此时进入锁膨胀阶段
    • 线程0再次执行synchronized(锁重入,有点类似于函数内部调用另外一个函数),再添加一条 Lock Record 作为重入的计数(栈的结构)
  • step3: 执行完临界区代码
    • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
      • CAS成功,则解锁成功
      • CAS失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2-8-2 锁膨胀的理解:将轻量级锁变为重量级锁(结合2-8-1)

发生场景实例:当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

02 Java的synchronized原理与Monitor对象

02 Java的synchronized原理与Monitor对象

step1: Thread1 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,自己进入 Monitor 的 EntryList 阻塞等待

step2:当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,必定失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

2-8-3 自旋优化

定义:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。(简单的理解为发现其他线程占着坑位,这个线程没有立刻阻塞而是多等了会)

注意点:

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
  • 自旋功能我们无需操作,Java 7 之后不能控制是否开启自旋功能
2-8-4 偏向锁

为什么需要偏向锁?

  • 轻量级锁
    • 优点:轻量级别锁通过线程栈帧中的锁记录结构替代重量级锁,不需要关联monitor对象。
    • 缺点:单个线程(没有其他线程与其竞争)使用轻量级锁,在锁重入的时候仍然需要执行CAS操作(栈帧中添加一个新的lock record,见下图,会有资源浪费)。

偏向锁为了克服轻量级锁的缺点而提出的

  • 锁重入:同一线程多次对同一对象加锁。

02 Java的synchronized原理与Monitor对象

会发生锁重入的代码:

static final Object obj = new Object();
public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}

public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}

public static void m3() {
    synchronized( obj ) {
    // 同步块 C
    }
}
  • 偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
    • 注意点:由于第一次CAS将线程ID设置到加锁对象的对象头的mark word中,发生锁重入的后,就不会再额外产生锁记录。

02 Java的synchronized原理与Monitor对象

2-8-5 偏向状态

偏向状态可以通过对象头的mark work反应出来,观察64位虚拟机的mark word,如下所示:

02 Java的synchronized原理与Monitor对象

总体上有5种状态,可以通过mark word最后2位判断当前对象的状态。

state 说明
Normal(正常状态) biased_lock为0表示没有被加偏向锁
Biased(偏向状态) biased_lock为1表示被加偏向锁,thread用于存储线程id,注意该id时os层面(非jvm)
Lightweight Locked(轻量级别的锁) ptr_to_lock_record指向加锁线程栈帧中的锁记录
Heavyweight Locked(重量锁) ptr_to_heavyweight_monitor指向加锁对象所关联的monitor对象

偏向锁的一些琐碎知识;

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
    thread、epoch、age 都为 0 (对象创建后的默认状态是偏向状态)
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效(需要等一段时间,比如几s),如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
    age 都为 0,第一次用到 hashcode 时才会赋值。
2-8-6 对象何时会撤销偏向状态(3种情况,待理解补充)
  • 调用对象 hashCode 方法,由于偏向状态无法存储hash值
  • 其他线程使用对象
    • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用wait/notify

参考资料

并发编程课程


20210224