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對象。
實例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對象。

  • 數組對象:對象頭除了包含mark word以及Kclass word還有數組長度

實例:32位虛擬機下,int類型只占用4字節,而Integer占用4+8字節,其中8字節是對象頭

Monitor(管程)的基本概念:

  • 管程:指的是管理共享變量以及對共享變量的操作過程,讓他們支持並發。
  • Java中的monitor:每個Java對象都可以關聯一個monitor對象,如果對一個對象使用synchronized關鍵字,那個這個對象的對象頭的mark word就被設置指向monitor對象的指針。

上圖中原理講解:

  • 線程2執行synchronized(obj)會檢查關聯到Monitor對象中的owner為null,將owner設置為自己,每一個Monitor對象只能有一個owner。
  • 線程1和線程3執行到臨界區代碼后,同樣檢查Monitor對象中的owner,由於Monitor對象存在owner,所以進入Entrylist (阻塞隊列)進行等待。

Synchronized字節碼層面理解

總結:

  • 字節碼第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次加鎖。

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 已經對該對象加了輕量級鎖

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,見下圖,會有資源浪費)。

偏向鎖為了克服輕量級鎖的缺點而提出的

  • 鎖重入:同一線程多次對同一對象加鎖。

會發生鎖重入的代碼:

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中,發生鎖重入的后,就不會再額外產生鎖記錄。

2-8-5 偏向狀態

偏向狀態可以通過對象頭的mark work反應出來,觀察64位虛擬機的mark word,如下所示:

總體上有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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM