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