鎖作為并發(fā)共享數(shù)據(jù)石景,保證一致性的工具,在JAVA平臺有多種實現(xiàn)(如 synchronized 和 ReentrantLock等等 ) 拙吉。這些已經(jīng)寫好提供的鎖為我們開發(fā)提供了便利潮孽,但是鎖的具體性質(zhì)以及類型卻很少被提及。本系列文章將分析JAVA中常見的鎖以及其特性筷黔,為大家答疑解惑恩商。
- 自旋鎖
- 自旋鎖的其他種類
- 阻塞鎖
- 可重入鎖
- 讀寫鎖
- 互斥鎖
- 悲觀鎖
- 樂觀鎖
- 公平鎖
- 非公平鎖
- 顯示鎖
- 內(nèi)置鎖
- 對象鎖
- 線程鎖
- 私有鎖
- 獨享鎖
- 共享鎖
- 鎖粗化
- 偏向鎖
- 輕量級鎖
- 重量級鎖
- 鎖膨脹
- 鎖消除
- 信號量
1、自旋鎖
自旋鎖是采用讓當(dāng)前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的必逆,當(dāng)循環(huán)的條件被其他線程改變時 才能進入臨界區(qū)怠堪。如下
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
使用了CAS原子操作揽乱,lock函數(shù)將owner設(shè)置為當(dāng)前線程,并且預(yù)測原來的值為空粟矿。unlock函數(shù)將owner設(shè)置為null凰棉,并且預(yù)測值為當(dāng)前線程。
當(dāng)有第二個線程調(diào)用lock操作時由于owner值不為空陌粹,導(dǎo)致循環(huán)一直被執(zhí)行撒犀,直至第一個線程調(diào)用unlock函數(shù)將owner設(shè)置為null,第二個線程才能進入臨界區(qū)掏秩。
由于自旋鎖只是將當(dāng)前線程不停地執(zhí)行循環(huán)體或舞,不進行線程狀態(tài)的改變,所以響應(yīng)速度更快蒙幻。但當(dāng)線程數(shù)不停增加時映凳,性能下降明顯,因為每個線程都需要執(zhí)行邮破,占用CPU時間诈豌。如果線程競爭不激烈,并且保持鎖的時間段抒和。適合使用自旋鎖矫渔。
注:該例子為非公平鎖,獲得鎖的先后順序摧莽,不會按照進入lock的先后順序進行庙洼。
2、自旋鎖的其他種類
在自旋鎖中 另有三種常見的鎖形式:TicketLock 镊辕,CLHlock 和MCSlock油够。
- TicketLock 主要解決的是訪問順序(公平性)的問題
思路:類似銀行辦業(yè)務(wù),先取一個號丑蛤,然后等待叫號叫到自己叠聋。好處:保證FIFO,先取號的肯定先進入受裹。
class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger(0);
private AtomicInteger ticketNum = new AtomicInteger(0);
private static final ThreadLocal<Integer> myNum = new ThreadLocal<Integer>();
public void lock () {
myNum.set(ticketNum.getAndIncrement());
while (serviceNum.get() != myNum.get()) {
};
}
public void unlock() {
serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
}
}
每次都要查詢一個serviceNum 服務(wù)號碌补,影響性能(必須要到主內(nèi)存讀取,并阻止其他cpu修改)棉饶。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isLocked = true;
}
@SuppressWarnings("unused")
private volatile CLHNode tail;
private static final ThreadLocal<CLHNode> LOCAL= new ThreadLocal<CLHNode>();
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class, "tail");
public void lock() {
CLHNode node = new CLHNode();
LOCAL.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node);
if (preNode != null) {
while (preNode.isLocked) {
}
preNode = null;
LOCAL.set(node);
}
}
public void unlock() {
CLHNode node = LOCAL.get();
if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
node = null;
}
}
CLH好處
1)公平厦章,F(xiàn)IFO,先來后到的順序進入鎖
2)而且沒有競爭同一個變量照藻,因為每個線程只要等待自己的前繼釋放就好了袜啃。
- MCS
CLH鎖并不是完美的,因為每個線程都是在前驅(qū)節(jié)點的locked字段上自旋幸缕。
MCS與CLH最大的不同在于:CLH是在前驅(qū)節(jié)點的locked域上自旋群发,MCS是在自己節(jié)點上的locked域上自旋晰韵。
具體的實現(xiàn)是,前驅(qū)節(jié)點在釋放鎖之后熟妓,會主動將后繼節(jié)點的locked域更新雪猪。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isLocked = true;
}
private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();
@SuppressWarnings("unused")
private volatile MCSNode queue;
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class, "queue");
public void lock() {
MCSNode currentNode = new MCSNode();
NODE.set(currentNode);
MCSNode preNode = UPDATER.getAndSet(this, currentNode);
if (preNode != null) {
preNode.next = currentNode;
while (currentNode.isLocked) {
}
}
}
public void unlock() {
MCSNode currentNode = NODE.get();
if (currentNode.next == null) {
if (UPDATER.compareAndSet(this, currentNode, null)) {
} else {
while (currentNode.next == null) {
}
}
} else {
currentNode.next.isLocked = false;
currentNode.next = null;
}
}
}
自旋鎖可能引起的問題:
1.過多占據(jù)CPU時間:如果鎖的當(dāng)前持有者長時間不釋放該鎖,那么等待者將長時間的占據(jù)cpu時間片起愈,導(dǎo)致CPU資源的浪費只恨,因此可以設(shè)定一個時間,當(dāng)鎖持有者超過這個時間不釋放鎖時抬虽,等待者會放棄CPU時間片阻塞官觅;
2.死鎖問題:試想一下,有一個線程連續(xù)兩次試圖獲得自旋鎖(比如在遞歸程序中)阐污,第一次這個線程獲得了該鎖休涤,當(dāng)?shù)诙卧噲D加鎖的時候,檢測到鎖已被占用(其實是被自己占用)疤剑,那么這時滑绒,線程會一直等待自己釋放該鎖闷堡,而不能繼續(xù)執(zhí)行隘膘,這樣就引起了死鎖。因此遞歸程序使用自旋鎖應(yīng)該遵循以下原則:遞歸程序決不能在持有自旋鎖時調(diào)用它自己杠览,也決不能在遞歸調(diào)用時試圖獲得相同的自旋鎖弯菊。
3、阻塞鎖
阻塞鎖踱阿,與自旋鎖不同管钳,改變了線程的運行狀態(tài)。
在JAVA環(huán)境中软舌,線程Thread有如下幾個狀態(tài):
新建狀態(tài)
就緒狀態(tài)
運行狀態(tài)
阻塞狀態(tài)
死亡狀態(tài)
阻塞鎖才漆,可以說是讓線程進入阻塞狀態(tài)進行等待,當(dāng)獲得相應(yīng)的信號(喚醒佛点,時間) 時醇滥,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程超营,通過競爭鸳玩,進入運行狀態(tài)。
JAVA中演闭,能夠進入\退出不跟、阻塞狀態(tài)或包含阻塞鎖的方法有 ,synchronized 關(guān)鍵字(其中的重量鎖)米碰,ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c經(jīng)常使用)
下面是一個JAVA 阻塞鎖實例
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.LockSupport;
public class CLHLock1 {
public static class CLHNode {
private volatile Thread isLocked;
}
@SuppressWarnings("unused")
private volatile CLHNode tail;
private static final ThreadLocal<CLHNode> LOCAL= new ThreadLocal<CLHNode>();
private static final AtomicReferenceFieldUpdater<CLHLock1, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock1.class,CLHNode.class, "tail");
public void lock() {
CLHNode node = new CLHNode();
LOCAL.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node);
if (preNode != null) {
preNode.isLocked = Thread.currentThread();
LockSupport.park(this);
preNode = null;
LOCAL.set(node);
}
}
public void unlock() {
CLHNode node = LOCAL.get();
if (!UPDATER.compareAndSet(this, node, null)) {
System.out.println("unlock\t" + node.isLocked.getName());
LockSupport.unpark(node.isLocked);
}
node = null;
}
}
阻塞鎖的優(yōu)勢在于,阻塞的線程不會占用cpu時間缚态, 不會導(dǎo)致 CPu占用率過高疟赊,但進入時間以及恢復(fù)時間都要比自旋鎖略慢。
4子刮、可重入鎖
可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數(shù)獲得鎖之后 篷帅,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響拴泌。
在JAVA環(huán)境下 ReentrantLock 和synchronized 都是可重入鎖魏身。
public class Test implements Runnable{
public synchronized void get(){
System.out.println(Thread.currentThread().getId());
set();
}
public synchronized void set(){
System.out.println(Thread.currentThread().getId());
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
Test ss=new Test();
new Thread(ss).start();
new Thread(ss).start();
new Thread(ss).start();
}
}
public class Test implements Runnable {
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
Test ss = new Test();
new Thread(ss).start();
new Thread(ss).start();
new Thread(ss).start();
}
}
兩個例子最后的結(jié)果都是正確的,即 同一個線程id被連續(xù)輸出兩次蚪腐。
結(jié)果如下:
Threadid: 8
Threadid: 8
Threadid: 10
Threadid: 10
Threadid: 9
Threadid: 9
可重入鎖最大的作用是避免死鎖箭昵。
我們以自旋鎖作為例子。
public class SpinLock {
private AtomicReference<Thread> owner =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
對于自旋鎖來說回季,
1家制、若有同一線程兩調(diào)用lock() ,會導(dǎo)致第二次調(diào)用lock位置進行自旋泡一,產(chǎn)生了死鎖
說明這個鎖并不是可重入的颤殴。(在lock函數(shù)內(nèi),應(yīng)驗證線程是否為已經(jīng)獲得鎖的線程)
2鼻忠、若1問題已經(jīng)解決涵但,當(dāng)unlock()第一次調(diào)用時,就已經(jīng)將鎖釋放了帖蔓。實際上不應(yīng)釋放鎖矮瘟。
(采用計數(shù)次進行統(tǒng)計)
修改之后,如下:
public class SpinLock1 {
private AtomicReference<Thread> owner =new AtomicReference<>();
private int count =0;
public void lock(){
Thread current = Thread.currentThread();
if(current==owner.get()) {
count++;
return ;
}
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
if(current==owner.get()){
if(count!=0){
count--;
}else{
owner.compareAndSet(current, null);
}
}
}
}
該自旋鎖即為可重入鎖塑娇。
5澈侠、讀寫鎖
與傳統(tǒng)鎖不同的是讀寫鎖的規(guī)則是可以共享讀,但只能一個寫埋酬,總結(jié)起來為:讀讀不互斥哨啃,讀寫互斥,寫寫互斥奇瘦,而一般的獨占鎖是:讀讀互斥棘催,讀寫互斥,寫寫互斥耳标,而場景中往往讀遠遠大于寫醇坝,讀寫鎖就是為了這種優(yōu)化而創(chuàng)建出來的一種機制。
簡單實現(xiàn)
public class ReadWriteLock {
/**
* 讀鎖持有個數(shù)
*/
private int readCount = 0;
/**
* 寫鎖持有個數(shù)
*/
private int writeCount = 0;
/**
* 獲取讀鎖,讀鎖在寫鎖不存在的時候才能獲取
*/
public synchronized void lockRead() throws InterruptedException{
// 寫鎖存在,需要wait
while (writeCount > 0) {
wait();
}
readCount++;
}
/**
* 釋放讀鎖
*/
public synchronized void unlockRead() {
readCount--;
notifyAll();
}
/**
* 獲取寫鎖,當(dāng)讀鎖存在時需要wait.
*/
public synchronized void lockWrite() throws InterruptedException{
// 先判斷是否有寫請求
while (writeCount > 0) {
wait();
}
// 此時已經(jīng)不存在獲取寫鎖的線程了,因此占坑,防止寫鎖饑餓
writeCount++;
// 讀鎖為0時獲取寫鎖
while (readCount > 0) {
wait();
}
}
/**
* 釋放讀鎖
*/
public synchronized void unlockWrite() {
writeCount--;
notifyAll();
}
}
6、互斥鎖
互斥鎖, 指的是一次最多只能有一個線程持有的鎖呼猪。
7画畅、悲觀鎖與樂觀鎖
悲觀鎖(Pessimistic Lock), 顧名思義就是很悲觀,每次去拿數(shù)據(jù)的時候都認為別人會修改宋距,所以每次在拿數(shù)據(jù)的時候都會上鎖轴踱,這樣別人想拿這個數(shù)據(jù)就會block直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機制谚赎,比如行鎖淫僻,表鎖等,讀鎖壶唤,寫鎖等雳灵,都是在做操作之前先上鎖。獨占鎖是悲觀鎖的一種實現(xiàn)闸盔。
樂觀鎖(Optimistic Lock), 顧名思義悯辙,就是很樂觀,每次去拿數(shù)據(jù)的時候都認為別人不會修改迎吵,所以不會上鎖躲撰,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號等機制击费。樂觀鎖適用于多讀的應(yīng)用類型拢蛋,這樣可以提高吞吐量,像數(shù)據(jù)庫如果提供類似于write_condition機制的其實都是提供的樂觀鎖荡灾。使用CAS來保證,保證這個操作的原子性瓤狐。
兩種鎖各有優(yōu)缺點瞬铸,不可認為一種好于另一種批幌,像樂觀鎖適用于寫比較少的情況下,即沖突真的很少發(fā)生的時候嗓节,這樣可以省去了鎖的開銷荧缘,加大了系統(tǒng)的整個吞吐量。但如果經(jīng)常產(chǎn)生沖突拦宣,上層應(yīng)用會不斷的進行retry截粗,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適鸵隧。
悲觀鎖代碼:
public class PessimisticLock implements Runnable {
//注:零長度的byte數(shù)組對象創(chuàng)建起來將比任何對象都經(jīng)濟
//編譯后的字節(jié)碼:生成零長度的byte[]對象只需3條操作碼绸罗,
//Object lock = new Object()則需要7行操作碼。
private static byte[] lock = new byte[0];
static int count = 0;
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new PessimisticLock());
Thread t2 = new Thread(new PessimisticLock());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
樂觀鎖代碼:
public class OptimisticLock implements Runnable {
private static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new OptimisticLock());
Thread t2 = new Thread(new OptimisticLock());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
8豆瘫、公平鎖與非公平鎖
公平鎖
在并發(fā)環(huán)境中珊蟀,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空外驱,或者當(dāng)前線程是等待隊列的第一個育灸,就占有鎖腻窒,否則就會加入到等待隊列中,以后會按照FIFO的規(guī)則從隊列中獲取鎖磅崭。
公平鎖的優(yōu)點是等待鎖的線程不會餓死儿子。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞砸喻,CPU喚醒阻塞線程的開銷比非公平鎖大柔逼。非公平鎖
首先直接嘗試占有鎖,如果嘗試失敗割岛,就再采用類似公平鎖那種方式卒落。
非公平鎖的優(yōu)點是可以減少喚起線程的開銷,整體的吞吐效率高蜂桶,因為線程有幾率不阻塞直接獲得鎖儡毕,CPU不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死扑媚,或者等很久才會獲得鎖腰湾。
9、顯示鎖和內(nèi)置鎖
顯示鎖用Lock來定義疆股、內(nèi)置鎖用syschronized费坊。
內(nèi)置鎖:每個java對象都可以用做一個實現(xiàn)同步的鎖,這些鎖成為內(nèi)置鎖旬痹。線程進入同步代碼塊或方法的時候會自動獲得該鎖附井,在退出同步代碼塊或方法時會釋放該鎖。獲得內(nèi)置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法两残。
內(nèi)置鎖是互斥鎖永毅。
10、對象人弓、類鎖沼死、私有鎖
類鎖:在代碼中的方法上加了static和synchronized的鎖,或者synchronized(xxx.class)的代碼段崔赌,如下文中的increament()意蛀;
對象鎖:在代碼中的方法上加了synchronized的鎖,或者synchronized(this)的代碼段健芭,如下文中的synOnMethod()和synInMethod()县钥;
私有鎖:在類內(nèi)部聲明一個私有屬性如private Object lock,在需要加鎖的代碼段synchronized(lock)慈迈,如下文中的synMethodWithObj()若贮。
代碼測試:
a.編寫一個啟動類ObjectLock
public class ObjectLock {
public static void main(String[] args) {
System.out.println("start time = " + System.currentTimeMillis()+"ms");
LockTestClass test = new LockTestClass();
for (int i = 0; i < 3; i++) {
Thread thread = new ObjThread(test, i);
thread.start();
}
}
}
b.編寫一個線程類ObjThread,用于啟動同步方法(注意它的run方法可能會調(diào)整以進行不同的測試)
public class ObjThread extends Thread {
LockTestClass lock;
int i = 0;
public ObjThread(LockTestClass lock, int i) {
this.lock = lock;
this.i = i;
}
public void run() {
//無鎖方法
//lock.noSynMethod(this.getId(),this);
//對象鎖方法1,采用synchronized synInMethod的方式
//lock.synInMethod();
//對象鎖方法2兜看,采用synchronized(this)的方式
lock.synOnMethod();
//私有鎖/對象鎖方法锥咸,采用synchronized(object)的方式
//lock.synMethodWithObj();
//私有鎖/類鎖方法,采用synchronized(static object)的方式
//lock.synMethodWithObj1();
//類鎖方法细移,采用static synchronized increment的方式
LockTestClass.increament();
}
}
c.再編寫一個鎖的測試類LockTestClass搏予,包括各種加鎖方法
public class LockTestClass {
//用于類鎖計數(shù)
private static int i = 0;
//私有鎖
private byte[] object = new byte[0];
//私有鎖
private static byte[] lock = new byte[0];
/**
* 無鎖方法
* @param threadID
* @param thread
*/
public void noSynMethod(long threadID, ObjThread thread) {
System.out.println("nosyn: class obj is " + thread + ", threadId is"
+ threadID);
}
/**
* 對象鎖方法1
*/
public synchronized void synOnMethod() {
System.out.println("synOnMethod begins" + ", time = "
+ System.currentTimeMillis() + "ms");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synOnMethod ends");
}
/**
* 對象鎖方法2,采用synchronized (this)來加鎖
*/
public void synInMethod() {
synchronized (this) {
System.out.println("synInMethod begins" + ", time = "
+ System.currentTimeMillis() + "ms");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synInMethod ends");
}
}
/**
* 對象鎖方法3
*/
public void synMethodWithObj() {
synchronized (object) {
System.out.println("synMethodWithObj begins" + ", time = "
+ System.currentTimeMillis() + "ms");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synMethodWithObj ends");
}
}
/**
* 類鎖
*/
public void synMethodWithObj1() {
synchronized (lock) {
System.out.println("synMethodWithObj1 synchronized begins" + ", time = "
+ System.currentTimeMillis() + "ms");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synMethodWithObj1 synchronized ends");
}
}
/**
* 類鎖
*/
public static synchronized void increament() {
System.out.println("class synchronized. i = " + i + ", time = "
+ System.currentTimeMillis() + "ms");
i++;
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("class synchronized ends.");
}
}
終端輸出:
start time = 1413101360231ms
synInMethod begins, time = 1413101360233ms
synInMethod ends
class synchronized. i = 0, time = 1413101362233ms
synInMethod begins, time = 1413101362233ms
class synchronized ends.
synInMethod ends
class synchronized. i = 1, time = 1413101364233ms
synInMethod begins, time = 1413101364233ms
class synchronized ends.
synInMethod ends
class synchronized. i = 2, time = 1413101366234ms
class synchronized ends.
可以看到對象鎖方法(synInMothod)第一次啟動時比類鎖方法(increament)快2秒,這是因為在synInMehtod執(zhí)行時sleep了2秒再執(zhí)行的increament弧轧,而這兩個方法共用一個線程雪侥,所以會慢2秒,如果increament在run中放到synInMethod前面精绎,那么第一次啟動時就是increament快2秒速缨。
而當(dāng)類鎖方法啟動時,另一個線程時的對象鎖方法也幾乎同時啟動代乃,說明二者使用的并非同一個鎖旬牲,不會產(chǎn)生競爭。
結(jié)論:類鎖和對象鎖不會產(chǎn)生競爭搁吓,二者的加鎖方法不會相互影響原茅。
同理可得:
類鎖、對象鎖堕仔、私有鎖之間不會產(chǎn)生競爭擂橘,加鎖方法不會相互影響。
總結(jié):
- 無論synchronized關(guān)鍵字加在方法上還是對象上摩骨,如果它作用的對象是非靜態(tài)的通贞,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態(tài)方法或一個類恼五,則它取得的鎖是對類昌罩,該類所有的對象同一把鎖。
- 每個對象只有一個鎖(lock)與之相關(guān)聯(lián)唤冈,誰拿到這個鎖誰就可以運行它所控制的那段代碼峡迷。
- 實現(xiàn)同步是要很大的系統(tǒng)開銷作為代價的,甚至可能造成死鎖你虹,所以盡量避免無謂的同步控制。
注:
調(diào)用對象wait()方法時彤避,會釋放持有的對象鎖傅物,以便于調(diào)用notify方法使用。notify()調(diào)用之后琉预,會等到notify所在的線程執(zhí)行完之后再釋放鎖董饰。
11、獨享鎖、共享鎖
獨享鎖
是指該鎖一次只能被一個線程所持有卒暂。共享鎖
是指該鎖可被多個線程所持有啄栓。ReentrantLock是獨享鎖。
Lock的另一個實現(xiàn)類ReadWriteLock也祠,其讀鎖是共享鎖昙楚,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的诈嘿,讀寫堪旧,寫讀 ,寫寫的過程是互斥的奖亚。
獨享鎖與共享鎖也是通過AQS來實現(xiàn)的淳梦,通過實現(xiàn)不同的方法,來實現(xiàn)獨享或者共享昔字。
12爆袍、線程鎖
鎖的統(tǒng)稱。多個線程同時對同一個對象進行讀寫操作,很容易會出現(xiàn)一些難以預(yù)料的問題作郭。所以我們需要給代碼塊加鎖,同一時刻只允許一個線程對某個對象進行操作螃宙。
13、鎖粗化
鎖粗化的概念應(yīng)該比較好理解所坯,就是將多次連接在一起的加鎖谆扎、解鎖操作合并為一次,將多個連續(xù)的鎖擴展成一個范圍更大的鎖芹助。舉個例子:
public class StringBufferTest {
StringBuffer stringBuffer = new StringBuffer();
public void append(){
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
}
}
這里每次調(diào)用stringBuffer.append方法都需要加鎖和解鎖堂湖,如果虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會將其合并成一次范圍更大的加鎖和解鎖操作状土,即在第一次append方法時進行加鎖无蜂,最后一次append方法結(jié)束后進行解鎖。
14蒙谓、偏向鎖斥季、輕量級鎖、重量級鎖累驮、鎖膨脹
鎖的狀態(tài)總共有四種:無鎖狀態(tài)酣倾、偏向鎖、輕量級鎖和重量級鎖谤专。隨著鎖的競爭躁锡,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的置侍,也就是說只能從低到高升級映之,不會出現(xiàn)鎖的降級)拦焚。JDK 1.6中默認是開啟偏向鎖和輕量級鎖的。
- 鎖膨脹:從輕量鎖膨脹到重量級鎖是在輕量級鎖解鎖過程發(fā)生的杠输。
- 重量級鎖:Synchronized是通過對象內(nèi)部的一個叫做監(jiān)視器鎖(monitor)來實現(xiàn)的赎败。但是監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的。而操作系統(tǒng)實現(xiàn)線程之間的切換這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)蠢甲,這個成本非常高僵刮,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,這就是為什么Synchronized效率低的原因峡钓。因此妓笙,這種依賴于操作系統(tǒng)Mutex Lock所實現(xiàn)的鎖我們稱之為“重量級鎖”。
- 輕量級鎖:“輕量級”是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的能岩。但是寞宫,首先需要強調(diào)一點的是,輕量級鎖并不是用來代替重量級鎖的拉鹃,它的本意是在沒有多線程競爭的前提下辈赋,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗。在解釋輕量級鎖的執(zhí)行過程之前膏燕,先明白一點钥屈,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的情況,如果存在同一時間訪問同一鎖的情況坝辫,就會導(dǎo)致輕量級鎖膨脹為重量級鎖篷就。
- 偏向鎖:引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令近忙,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖竭业,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。上面說過及舍,輕量級鎖是為了在線程交替執(zhí)行同步塊時提高性能未辆,而偏向鎖則是在只有一個線程執(zhí)行同步塊時進一步提高性能。
- 無鎖狀態(tài):在代碼進入同步塊的時候锯玛,如果同步對象鎖狀態(tài)為無鎖狀態(tài)咐柜。
14.1、底層原理
- java對象頭
synchronized用的鎖是存在Java對象頭里的攘残,那么什么是Java對象頭呢拙友?Hotspot虛擬機的對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段)、Class Pointer(類型指針)肯腕。其中Class Point是是對象指向它的類元數(shù)據(jù)的指針献宫,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用于存儲對象自身的運行時數(shù)據(jù)实撒,它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵姊途。
Mark Word用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)知态、GC分代年齡捷兰、鎖狀態(tài)標志、線程持有的鎖负敏、偏向線程 ID贡茅、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中其做,1個機器碼等于4字節(jié)顶考,也就是32bit),但是如果對象是數(shù)組類型妖泄,則需要三個機器碼驹沿,因為JVM虛擬機可以通過Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法從數(shù)組的元數(shù)據(jù)來確認數(shù)組的大小蹈胡,所以用一塊來記錄數(shù)組長度渊季。下圖是Java對象頭的存儲結(jié)構(gòu)(32位虛擬機)。
- Monitor
Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu)罚渐,每一個線程都有一個可用monitor record列表却汉,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關(guān)聯(lián)(對象頭的MarkWord中的LockWord指向monitor的起始地址)荷并,同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識合砂,表示該鎖被這個線程占用。
下面是鎖代碼塊 示例代碼以及對應(yīng)class文件信息:
public class Test {
private final static Object lockHelper = new Object();
public static void main(String[] args) {
System.out.println("Hello World");
synchronized (lockHelper) {
System.out.println("insert Syn...");
}
}
}
以上源织,可知同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令翩伪。
其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置雀鹃,當(dāng)執(zhí)行monitorenter指令時幻工,當(dāng)前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán),當(dāng) monitor 的進入計數(shù)器為 0黎茎,那線程可以成功取得 monitor囊颅,并將計數(shù)器值設(shè)置為 1,取鎖成功傅瞻。如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán)踢代,那它可以重入這個 monitor,重入時計數(shù)器的值也會加 1嗅骄。倘若其他線程已經(jīng)擁有 monitor 的所有權(quán)胳挎,那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢溺森,即monitorexit指令被執(zhí)行慕爬,執(zhí)行線程將釋放 monitor(鎖)并設(shè)置計數(shù)器值為0 窑眯,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成医窿,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令磅甩,而無論這個方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行姥卢,編譯器會自動產(chǎn)生一個異常處理器卷要,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令独榴。從字節(jié)碼中也可以看出多了一個monitorexit指令僧叉,它就是異常結(jié)束時被執(zhí)行的釋放monitor 的指令。這便是synchronized鎖在同步代碼塊上實現(xiàn)的基本原理棺榔。
下面是鎖方法 示例代碼以及對應(yīng)class文件信息:
public class Test {
private final static Object lockHelper = new Object();
public synchronized void test(){
System.out.println("test syn");
}
}
以上瓶堕,synchronized修飾的方法并沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識掷豺,該標識指明了該方法是一個同步方法捞烟,JVM通過該ACC_SYNCHRONIZED訪問標志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用当船。這便是synchronized鎖在同步方法上實現(xiàn)的基本原理题画。
14.2、偏向鎖
背景:大多數(shù)情況下德频,鎖總是由同一線程多次獲得苍息,因此為了減少同一線程獲取鎖的代價而引入偏向鎖。
核心思想:如果一個線程獲得了鎖壹置,那么鎖就進入偏向模式竞思,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個線程再次請求鎖時可直接獲取鎖钞护,省去了大量有關(guān)鎖申請的操作盖喷,從而提高程序的性能。
引入偏向鎖主要目的是:為了在沒有多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑难咕。因為輕量級鎖的加鎖解鎖操作是需要依賴多次CAS原子指令的课梳,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗也必須小于節(jié)省下來的CAS原子指令的性能消耗)余佃。
那么偏向鎖是如何來減少不必要的CAS操作呢暮刃?首先我們看下無競爭下鎖存在什么問題:
現(xiàn)在幾乎所有的鎖都是可重入的,即已經(jīng)獲得鎖的線程可以多次鎖住/解鎖監(jiān)視對象爆土,按照之前的HotSpot設(shè)計椭懊,每次加鎖/解鎖都會涉及到一些CAS操作(比如對等待隊列的CAS操作),CAS操作會延遲本地調(diào)用步势,因此偏向鎖的想法是 一旦線程第一次獲得了監(jiān)視對象氧猬,之后讓監(jiān)視對象“偏向”這個線程背犯,之后的多次調(diào)用則可以避免CAS操作,說白了就是置個變量狂窑,如果發(fā)現(xiàn)為true則無需再走各種加鎖/解鎖流程媳板。
CAS為什么會引入本地延遲桑腮?這要從SMP(對稱多處理器)架構(gòu)說起泉哈,下圖大概表明了SMP的結(jié)構(gòu):
其意思是所有的CPU會共享一條系統(tǒng)總線(BUS),靠此總線連接主存破讨。每個核都有自己的一級緩存丛晦,各核相對于BUS對稱分布,因此這種結(jié)構(gòu)稱為“對稱多處理器”提陶。
而CAS的全稱為Compare-And-Swap烫沙,是一條CPU的原子指令,其作用是讓CPU比較后原子地更新某個位置的值隙笆,經(jīng)過調(diào)查發(fā)現(xiàn)锌蓄,其實現(xiàn)方式是基于硬件平臺的匯編指令,就是說CAS是靠硬件實現(xiàn)的撑柔,JVM只是封裝了匯編調(diào)用瘸爽,那些AtomicInteger類便是使用了這些封裝后的接口。
例如:Core1和Core2可能會同時把主存中某個位置的值Load到自己的L1 Cache中铅忿,當(dāng)Core1在自己的L1 Cache中修改這個位置的值時剪决,會通過總線,使Core2中L1 Cache對應(yīng)的值“失效”檀训,而Core2一旦發(fā)現(xiàn)自己L1 Cache中的值失效(稱為Cache命中缺失)則會通過總線從內(nèi)存中加載該地址最新的值柑潦,大家通過總線的來回通信稱為“Cache一致性流量”,因為總線被設(shè)計為固定的“通信能力”峻凫,如果Cache一致性流量過大渗鬼,總線將成為瓶頸。而當(dāng)Core1和Core2中的值再次一致時荧琼,稱為“Cache一致性”譬胎,從這個層面來說,鎖設(shè)計的終極目標便是減少Cache一致性流量铭腕。
而CAS恰好會導(dǎo)致Cache一致性流量银择,如果有很多線程都共享同一個對象,當(dāng)某個Core CAS成功時必然會引起總線風(fēng)暴累舷,這就是所謂的本地延遲浩考,本質(zhì)上偏向鎖就是為了消除CAS,降低Cache一致性流量被盈。
結(jié)果:對于鎖競爭較少的場合析孽,偏向鎖有很好的優(yōu)化搭伤。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了(大多情況下每次申請鎖的線程不同)袜瞬,這種情況下使用偏向鎖會降低性能(Mark Word改變結(jié)構(gòu))怜俐。偏向鎖失敗后,會先升級為輕量級鎖邓尤。
實現(xiàn):
獲取鎖
獲取鎖的步驟如下:
a.檢測Mark Word是否為可偏向狀態(tài)(1|01【Mark Word最后兩位拍鲤,下文同】);
b.若為可偏向狀態(tài)汞扎,則測試線程ID是否為當(dāng)前線程ID季稳,如果是,則執(zhí)行步驟e澈魄,否則執(zhí)行步驟c景鼠;
c.如果線程ID不為當(dāng)前線程ID,則通過CAS操作競爭鎖痹扇,競爭成功铛漓,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行線程d鲫构;
d.通過CAS競爭鎖失敗浓恶,證明當(dāng)前存在多線程競爭情況,當(dāng)?shù)竭_全局安全點芬迄,獲得偏向鎖的線程被掛起问顷,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼塊禀梳;
e.執(zhí)行同步代碼塊釋放鎖
偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制杜窄,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭算途。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執(zhí)行的代碼)塞耕。其步驟如下:
a.暫停擁有偏向鎖的線程,判斷鎖對象石是否還處于被鎖定狀態(tài)嘴瓤;
b.撤銷偏向鎖扫外,恢復(fù)到無鎖狀態(tài)(01)或者輕量級鎖的狀態(tài);
具體過程如下圖所示:
14.3廓脆、輕量級鎖
背景:偏向鎖失敗筛谚,鎖升級為輕量級鎖,此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)停忿。
核心思想:根據(jù)經(jīng)驗得知:對絕大部分的鎖驾讲,在整個同步周期內(nèi)都不存在競爭。
引入輕量級鎖的主要目的是 在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗吮铭。當(dāng)關(guān)閉偏向鎖功能或者多個線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖时迫,則會嘗試獲取輕量級鎖
結(jié)果:輕量級鎖所適用于是線程交替執(zhí)行同步塊的場合。
實現(xiàn)
- 獲取鎖
獲取鎖的步驟如下:
a.判斷當(dāng)前對象是否處于無鎖狀態(tài)(0|01)谓晌,若是掠拳,則JVM首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝纸肉;否則執(zhí)行c溺欧;
b.JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖毁靶,則將鎖標志位變成00(表示此對象處于輕量級鎖狀態(tài))胧奔,執(zhí)行同步操作;如果失敗則執(zhí)行步驟c预吆;
c.判斷當(dāng)前對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對象的鎖胳泉,則直接執(zhí)行同步代碼塊拐叉;否則只能說明該鎖對象已經(jīng)被其他線程搶占了,這時輕量級鎖需要進行自適應(yīng)旋轉(zhuǎn)等待獲取鎖扇商。
主要步驟:
1.通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對象替換當(dāng)前的Mark Word凤瘦;
2.如果替換成功,整個同步過程就完成了案铺,恢復(fù)到無鎖狀態(tài)(01)蔬芥;
3.如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹)控汉,那就要在釋放鎖的同時笔诵,喚醒被掛起的線程;
-
釋放鎖
輕量級鎖的釋放也是通過CAS操作來進行的姑子,主要步驟如下:
a.取出在獲取輕量級鎖保存在Displaced Mark Word中的數(shù)據(jù)乎婿;
b.用CAS操作將取出的數(shù)據(jù)替換當(dāng)前對象的Mark Word中,如果成功街佑,則說明釋放鎖成功谢翎,否則執(zhí)行(3);
c.如果CAS操作替換失敗沐旨,說明有其他線程嘗試獲取該鎖森逮,則需要在釋放鎖的同時需要喚醒被掛起的線程。
具體過程如下圖所示:
為什么升級為輕量鎖時要把對象頭里的Mark Word復(fù)制到線程棧的鎖記錄中呢磁携?
因為在申請對象鎖時 需要以該值作為CAS的比較條件褒侧,同時在升級到重量級鎖的時候,能通過這個比較判定是否在持有鎖的過程中此鎖被其他線程申請過,如果被其他線程申請了璃搜,則在釋放鎖的時候要喚醒被掛起的線程拖吼。
- 為什么會嘗試CAS不成功以及什么情況下會不成功?
CAS本身是不帶鎖機制的这吻,其是通過比較而來吊档。假設(shè)如下場景:線程A和線程B都在對象頭里的鎖標識為無鎖狀態(tài)進入澎灸,那么如線程A先更新對象頭為其鎖記錄指針成功之后硕糊,線程B再用CAS去更新,就會發(fā)現(xiàn)此時的對象頭已經(jīng)不是其操作前的對象HashCode了私沮,所以CAS會失敗移怯。也就是說香璃,只有兩個線程并發(fā)申請鎖的時候會發(fā)生CAS失敗。
然后線程B進行CAS自旋舟误,等待對象頭的鎖標識重新變回?zé)o鎖狀態(tài)或?qū)ο箢^內(nèi)容等于對象HashCode(因為這是線程B做CAS操作前的值)葡秒,這也就意味著線程A執(zhí)行結(jié)束(參見后面輕量級鎖的撤銷,只有線程A執(zhí)行完畢撤銷鎖了才會重置對象頭)嵌溢,此時線程B的CAS操作終于成功了眯牧,于是線程B獲得了鎖以及執(zhí)行同步代碼的權(quán)限。如果線程A的執(zhí)行時間較長赖草,線程B經(jīng)過若干次CAS時鐘沒有成功学少,則鎖膨脹為重量級鎖,即線程B被掛起阻塞秧骑、等待重新調(diào)度版确。
14.4、重量級鎖
即早期的synchronized乎折,通過monitor實現(xiàn)绒疗,monitor依賴于底層的操作系統(tǒng)來實現(xiàn)的,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)笆檀,切換成本高忌堂。
14.5酗洒、各鎖的優(yōu)缺點及適用場景
15士修、鎖消除
鎖消除即刪除不必要的加鎖操作。根據(jù)代碼逃逸技術(shù)樱衷,如果判斷到一段代碼中棋嘲,堆上的數(shù)據(jù)不會逃逸出當(dāng)前線程,那么可以認為這段代碼是線程安全的矩桂,不必要加鎖沸移。看下面這段程序:
public class SynchronizedTest02 {
public static void main(String[] args) {
SynchronizedTest02 test02 = new SynchronizedTest02();
//啟動預(yù)熱
for (int i = 0; i < 10000; i++) {
i++;
}
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
test02.append("abc", "def");
}
System.out.println("Time=" + (System.currentTimeMillis() - start));
}
public void append(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
}
雖然StringBuffer的append是一個同步方法,但是這段程序中的StringBuffer屬于一個局部變量雹锣,并且不會從該方法中逃逸出去网沾,所以其實這過程是線程安全的,可以將鎖消除蕊爵。
16辉哥、信號量
線程同步工具:Semaphorehttp://www.reibang.com/p/b9b6cf064549