sychronized是java多線程非常關鍵的一個知識點,這篇博客將從synchronized幾個用法以及代碼來學習。 sychronized的作用是能夠保證同一時間只有一個線程來運行這塊代碼,達到併發效果,如果沒有保證併發的話,在多線程編碼中就會產生致命問題,比如經典的i++,這也是資料庫併發中 ...
sychronized是java多線程非常關鍵的一個知識點,這篇博客將從synchronized幾個用法以及代碼來學習。
sychronized的作用是能夠保證同一時間只有一個線程來運行這塊代碼,達到併發效果,如果沒有保證併發的話,在多線程編碼中就會產生致命問題,比如經典的i++,這也是資料庫併發中經典的案例,i++並不是原子操作,分為三步,取數,操作,寫數,參考這段代碼,可以運行一下看下結果
public class showUnsafe1 implements Runnable{
static int i=0;
@Override
public void run() {
for(int j=0;j<10000;j++){
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new showUnsafe1());
Thread thread2 = new Thread(new showUnsafe1());
thread1.start(); // 啟動thread1,在合適的時刻運行
thread2.start(); // 啟動thread2,在合適的時刻運行
thread1.join(); // 讓主線程等待thread1運行完
thread2.join(); // 讓主線程等待thread2運行完
System.out.println(i);
}
}
一、synchronized四種用法
synchronized為啥這麼神奇,無它,加鎖而已,不少八股文喜歡分為兩種鎖,一種是對象鎖,一種是類鎖,還可以分為方法鎖,代碼塊鎖,靜態鎖,class鎖,我們通過代碼學習他們如何使用
1.對象鎖:方法鎖
方法鎖是用synchronized修飾的一個類方法,作用方法即是方法作用域,除了這個方法要同步,其餘不需要
public class SynchronizedObjectMethod implements Runnable{
private static SynchronizedObjectMethod instance=new SynchronizedObjectMethod();
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()+"運行結束");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
2. 對象鎖:代碼塊形式
代碼塊鎖就是常用的同步方法塊,synchronized鎖住的是它裡面的對象,作用域就是synchonized{}裡面的代碼
public class SynchronizedObjectCodeBlock implements Runnable{
private static SynchronizedObjectCodeBlock instance=new SynchronizedObjectCodeBlock();
Object lock1=new Object();
@Override
public void run() {
synchronized (lock1){
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 thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
3. 類鎖:class形式
class形式說的是synchronized()括弧里使用的鎖是class對象,所謂class對象指得是java文件對應的一個java.lang.class對象,所有該類生成的對象共有這個class對象 類載入機制,所以這個鎖鎖住了這個類生成的所有對象
public class SynchronizedClassClass implements Runnable{
private static SynchronizedClassClass instance1=new SynchronizedClassClass();
private static SynchronizedClassClass instance2=new SynchronizedClassClass();
public void method(){
synchronized (SynchronizedClassClass.class){
System.out.println("我是類鎖的形式之一:修飾.class。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"運行結束");
}
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
在這個案例中,存在兩個SynchronizedClassClass對象,但是不能同時訪問同步代碼
4.類鎖:static形式
static形式說的是static修飾synchronized修飾的方法,即static synchronized methodName,作用方法還是這個類的class對象
public class SynchronizedClassStatic implements Runnable{
private static SynchronizedClassStatic instance1=new SynchronizedClassStatic();
private static SynchronizedClassStatic instance2=new SynchronizedClassStatic();
public static synchronized void method(){
System.out.println("我是類鎖的形式之一:加static修飾。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"運行結束");
}
public void method2(){
System.out.println("我是非靜態方法,我叫"+Thread.currentThread().getName());
}
@Override
public void run() {
method();
method2();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
二、synchronized的原理
讓我們從位元組碼來看看synchronized的神秘面紗, 我們反編譯synchronized修飾object的java文件
public class Decompilation {
private Object object=new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
反編譯指令為 javap -c -v YourName.java
所以就是這個小小的Monitor Enter和monitorexit指令完成了同步操作,關於Monitor Enter和monitorexit的定義可以查看jvm文檔 中的註釋
先看看 Monitor Enter,註意紫色標註
再看看Monitor Exit
這裡就解釋清楚為什麼synchronized上的鎖是可重入的,對於更深入的理解
重點還是提到的Monitor這個概念
我們再反編譯synchronize修飾的同步方法,其結果是
與之前不一樣,這裡是在方法的訪問標識上添加了ACC_SYNCHRONIZED,它在jvm文檔中是這麼解釋
大致意思就是當調用設置了 ACC_SYNCHRONIZED 的方法時,執行線程進入監視器(monitor),然後執行這個方法,方法執行完畢後退出監視器。在執行線程擁有監視器期間,沒有其他線程可以進入這個方法.
同樣,這裡涉及了Monitor
2.1 深入Monitor
雖然HotSpot的JDK代碼沒有開元,但好在還有OpenJDK,大家有時間可以看看ObjectMonitor.hpp和ObjectMonitor.cpp,如果c語言功力不好的同學,看看註釋,大致知道每個變數啥意思即可。
主要是_owner
,_recursions
,_entryList
,_waitSet
,此外還有header
這個對象頭將對象和Monitor
聯繫起來。
_owner
顧名思義就是鎖的擁有者,recursions
就是鎖的進入次數,初始為0,而_entryList
是存放Blocked狀態的線程的,waitSet
是存放Waiting狀態的線程。
在HotSpot中,一個對象是在Heap中的存儲佈局有三個部分:對象頭(Header),實例數據(Instance Data)以及對齊填充部分,而對象頭一般由兩個部分組成:MarkWord和類型指針,如果是數組對象,那麼還有數組長度信息。
而MarkWord就是連接Monitor和對象的關鍵東西。它存儲了對象運行時的一些數據,比如HashCode,GC年齡,鎖的狀態,線程所持有的鎖等。
總之,monitor才是synchronized併發的關鍵,monitor是底層用cpp實現的一個對象,實現了鎖的狀態轉換,獲取釋放等方法,通過markword與java對象聯繫一起,而markword是嵌入在java頭部的。
三、synchornized的各種鎖以及優化
3.1 鎖的升級
從jdk1.6開始,synchronized鎖有四種狀態,級別由低到高是
無鎖,偏向鎖,輕量鎖,重量鎖
鎖的升級過程是一個很麻煩的事情,本質就是如果發生了鎖的競爭就升級鎖,直到升級到重量鎖為止,期間使用到了CAS和自旋來避免線程直接進入阻塞狀態。有興趣的同學可以看看《阿裡巴巴java性能調優實戰》這本書。
3.2 JIT 實現鎖消除和鎖粗化
鎖消除的概念比較容易理解,就是如果編譯器認定一個鎖只會被單個線程訪問,那麼這個鎖就可以被消除。而鎖粗化,簡單的說就是JIT動態編譯時發現相鄰的同步塊使用的是同一個鎖實例,那麼就合併他們,避免頻繁加鎖釋放鎖。
3.3 減少鎖的粒度
這是我們平時編程的時候,我們可以控制的,有的同學(比如我)圖省事往往可以加對象鎖的,直接加類鎖,這樣就是不地道的。
《阿裡巴巴java性能調優實戰》舉例說明減少鎖的粒度的好處,比如被拋棄的HashTable和新寵ConcurrentHashMap的轉換,就是使用了減少鎖的粒度方法。