synchronized譬涡、lock的簡介
假設(shè)一個Integer類型的全局變量i同時被A闪幽,B,C三個線程訪問涡匀,A線程主要是給i做加1的操作盯腌,B線程主要是給i做減1的操作,C線程主要是讀取i的值并打印出來陨瘩。那么問題來了腕够,C線程打印的i值是沒有變了,還是已經(jīng)減1舌劳,或者已經(jīng)加1呢帚湘?
這里就涉及到線程同步的問題,線程同步是多個線程按照預(yù)定的先后次序來運行甚淡,Java中可以通過synchronized或者lock來實現(xiàn)線程的同步大诸,下面將主要介紹synchronized、lock的用法以及兩者的區(qū)別材诽。
1.synchronized
synchronized是Java中的關(guān)鍵字,使用synchronized能夠防止多個線程同時進入并訪問程序的臨界區(qū)(程序的某個方法或者代碼塊)恒傻。synchronized可以修飾方法或者代碼塊脸侥,當(dāng)A線程訪問被synchronized修飾的方法或者代碼塊時,A線程就獲取該對象的鎖盈厘,此時如果B線程想訪問該臨界區(qū)睁枕,就必須等待A線程執(zhí)行完畢并釋放該對象的鎖。
1)synchronized method():被synchronized修飾后沸手,該方法就變成了一個同步方法外遇,其作用范圍就是整個方法,而作用對象要分兩種情況來考慮契吉。
情況一:該方法是非靜態(tài)方法跳仿,其作用對象就是調(diào)用該方法的對象;
情況二:該方法是靜態(tài)方法捐晶,其作用對象就是調(diào)用該方法的所有類的實例對象菲语。
2)synchronized ():括號里可以是類或者對象。
synchronized(className.class):作用對象是訪問該代碼塊的該類所有對象惑灵,當(dāng)某個線程已經(jīng)在訪問該代碼塊時山上,其它該類的所有對象都不能訪問該代碼塊。
synchronized(object):是給object加鎖英支,其他線程訪問synchronized (object)同步代碼塊時將會被阻塞(同一個類的不同對象可以訪問該代碼塊)佩憾。
synchronized(this):作用對象是當(dāng)前對象,其他線程訪問該對象的同步方法塊時將會被阻塞(同一個類的不同對象可以訪問該代碼塊)。
下面給出一個簡單例子妄帘,通過synchronized關(guān)鍵字楞黄,兩個線程交替地輸出ABABABAB字符串,代碼如下:
public class PrintAB {
private final Object object = new Object();
private boolean flag = false;
public static void main(String[] args) {
PrintAB printA = new PrintAB();
MyRunnable1 myRunnable1 = printA.new MyRunnable1();
MyRunnable2 myRunnable2 = printA.new MyRunnable2();
Thread thread1 = new Thread(myRunnable1);
Thread thread2 = new Thread(myRunnable2);
thread1.start();
thread2.start();
}
public class MyRunnable1 implements Runnable {
@Override
public void run() {
while (true) {
synchronized (object) {
if (flag) {
try {
object.wait();
} catch (InterruptedException e) {
}
}
System.out.print('A');
flag = true;
object.notify();
}
}
}
}
public class MyRunnable2 implements Runnable {
@Override
public void run() {
while (true) {
synchronized (object) {
if (!flag) {
try {
object.wait();
} catch (InterruptedException e) {
}
}
System.out.print('B');
flag = false;
object.notify();
}
}
}
}
}
輸出結(jié)果是:
ABABABABABAB
synchronized的更為詳細的介紹可以參考Java多線程干貨系列synchronized
2.Lock
synchronized是Java語言的關(guān)鍵字寄摆,是內(nèi)置特性谅辣,而ReentrantLock是一個類(實現(xiàn)Lock接口的類),通過該類可以實現(xiàn)線程的同步婶恼。Lock是一個接口桑阶,源碼很簡單,主要是聲明了四個方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock一般的使用如下:
Lock lock= ...;//獲取鎖
lock.lock();
try{
//處理任務(wù)
}catch(Exception e){
}finally{
lock.unlock();//釋放鎖
}
lock()勾邦、tryLock()蚣录、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的,unLock()方法是用來釋放鎖的眷篇,其放在finally塊里執(zhí)行萎河,可以保證鎖一定被釋放,newCondition方法下面會做介紹(通過該方法可以生成一個Condition對象蕉饼,而Condition是一個多線程間協(xié)調(diào)通信的工具類)虐杯。
Lock接口的主要方法介紹:
lock():獲取不到鎖就不罷休,否則線程一直處于block狀態(tài)昧港。
tryLock():嘗試性地獲取鎖擎椰,不管有沒有獲取到都馬上返回,拿到鎖就返回true创肥,不然就返回false 达舒。
tryLock(long time, TimeUnit unit):如果獲取不到鎖,就等待一段時間叹侄,超時返回false巩搏。
lockInterruptibly():該方法稍微難理解一些,在說該方法之前趾代,先說說線程的中斷機制贯底,每個線程都有一個中斷標志,不過這里要分兩種情況說明:
1) 線程在sleep撒强、wait或者join丈甸, 這個時候如果有別的線程調(diào)用該線程的 interrupt()方法,此線程會被喚醒并被要求處理InterruptedException尿褪。
2)如果線程處在運行狀態(tài)睦擂, 則在調(diào)用該線程的interrupt()方法時,不會響應(yīng)該中斷杖玲。
lockInterruptibly()和上面的第一種情況是一樣的顿仇, 線程在獲取鎖被阻塞時,如果調(diào)用lockInterruptibly()方法,該線程會被喚醒并被要求處理InterruptedException臼闻。下面給出一個響應(yīng)中斷的簡單例子:
public class Test{
public static void main(String[] args){
MyRunnable myRunnable = new Test().new MyRunnable();
Thread thread1 = new Thread(myRunnable,"thread1");
Thread thread2 = new Thread(myRunnable,"thread2");
thread1.start();
thread2.start();
thread2.interrupt();
}
public class MyRunnable implements Runnable{
private Lock lock=new ReentrantLock();
@Override
public synchronized void run() {
try{
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() +"獲取了鎖");
Thread.sleep(5000);
}catch(InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() +"響應(yīng)中斷");
}
finally{
lock.unlock();
System.out.println(Thread.currentThread().getName() +"釋放了鎖");
}
}
}
}
輸出結(jié)果是:
thread1獲取了鎖
thread1釋放了鎖
thread2響應(yīng)中斷
thread2在響應(yīng)中斷后鸿吆,在finally塊里執(zhí)行unlock方法時,會拋出java.lang.IllegalMonitorStateException異常(因為thread2并沒有獲取到鎖述呐,只是在等待獲取鎖的時候響應(yīng)了中斷惩淳,這時再釋放鎖就會拋出異常)。
上面簡單介紹了ReentrantLock的使用乓搬,下面具體介紹使用ReentrantLock的中的newCondition方法實現(xiàn)一個生產(chǎn)者消費者的例子思犁。
生產(chǎn)者、消費者
例子:兩個線程A进肯、B激蹲,A生產(chǎn)牙刷并將其放到一個緩沖隊列中,B從緩沖隊列中購買(消費)牙刷(說明:緩沖隊列的大小是有限制的)江掩,這樣就會出現(xiàn)如下兩種情況学辱。
1)當(dāng)緩沖隊列已滿時,A并不能再生產(chǎn)牙刷环形,只能等B從緩沖隊列購買牙刷策泣;
2)當(dāng)緩沖隊列為空時,B不能再從緩沖隊列中購買牙刷抬吟,只能等A生產(chǎn)牙刷放到緩沖隊列后才能購買萨咕。
public class ToothBrushDemo {
public static void main (String[] args){
final ToothBrushBusiness toothBrushBusiness =
new ToothBrushDemo().new ToothBrushBusiness();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, true);
}
}, "牙刷生產(chǎn)者1").start();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, false);
}
}, "牙刷消費者1").start();
}
//循環(huán)執(zhí)行50次
public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
boolean isProducer){
for(int i = 0 ; i < 50 ; i++) {
if (isProducer) {
toothBrushBusiness.produceToothBrush();
} else {
toothBrushBusiness.consumeToothBrush();
}
}
}
public class ToothBrushBusiness {
//定義一個大小為10的牙刷緩沖隊列
private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
private int number = 1;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public ToothBrushBusiness() {
}
//生產(chǎn)牙刷
public void produceToothBrush(){
lock.lock();
try {
//牙刷緩沖隊列已滿,則生產(chǎn)牙刷線程等待
while (toothBrushQueue.isFull()){
notFull.await();
}
ToothBrush toothBrush = new ToothBrush(number);
toothBrushQueue.enQueue(toothBrush);
System.out.println("生產(chǎn): " + toothBrush.toString());
number++;
//牙刷緩沖隊列加入牙刷后,喚醒消費牙刷線程
notEmpty.signal();
}
catch (InterruptedException e){
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//消費牙刷
public void consumeToothBrush(){
lock.lock();
try {
//牙刷緩沖隊列為空,則消費牙刷線程等待
while (toothBrushQueue.isEmpty()){
notEmpty.await();
}
ToothBrush toothBrush = toothBrushQueue.deQueue();
System.out.println("消費: " + toothBrush.toString());
//從牙刷緩沖隊列取出牙刷后,喚醒生產(chǎn)牙刷線程
notFull.signal();
}
catch (InterruptedException e){
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ToothBrush {
private int number;
public ToothBrush(int number) {
this.number = number;
}
@Override
public String toString() {
return "牙刷編號{" +
"number=" + number +
'}';
}
}
}
這里緩沖隊列的大小設(shè)成了10,定義了一個可重入鎖lock拗军,兩個狀態(tài)標記對象notEmpty任洞,notFull蓄喇,分別用來標記緩沖隊列是否為空发侵,是否已滿。
1)當(dāng)緩沖隊列已滿時妆偏,調(diào)用notFull.await方法用來阻塞生產(chǎn)牙刷線程刃鳄。
2)當(dāng)緩沖隊列為空時,調(diào)用notEmpty.await方法用來阻塞購買牙刷線程钱骂。
3)notEmpty.signal用來喚醒消費牙刷線程叔锐,notFull.signal用來喚醒生產(chǎn)牙刷線程。
Object和Conditon對應(yīng)關(guān)系如下:
Object Condition
休眠 wait await
喚醒特定線程 notify signal
喚醒所有線程 notifyAll signalAll
對于同一個鎖见秽,我們可以創(chuàng)建多個Condition愉烙,就是多個監(jiān)視器的意思。在不同的情況下使用不同的Condition解取,Condition是被綁定到Lock上的步责,要創(chuàng)建一個Lock的Condition必須用newCondition()方法。
Lock鎖的介紹
ReentrantLock(可重入鎖)是唯一實現(xiàn)了Lock接口的類,并且ReentrantLock提供了更多的方法蔓肯。
synchronized和ReentrantLock都是可重入鎖遂鹊,可重入性舉個簡單的例子,當(dāng)一個線程執(zhí)行到某個synchronized方法時蔗包,比如說method1秉扑,而在method1中會調(diào)用另外一個synchronized方法method2,此時線程不必重新去申請鎖调限,而是可以直接執(zhí)行方法method2舟陆。
上面的響應(yīng)中斷的例子已經(jīng)地使用到了ReentrantLock,下面來介紹另外一種鎖旧噪,可重入讀寫鎖ReentrantReadWriteLock吨娜,該類實現(xiàn)了ReadWriteLock接口,該接口的源碼如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock接口只有獲取讀鎖和寫鎖的方法淘钟,而ReentrantReadWriteLock是實現(xiàn)了ReadWriteLock接口宦赠,接著對其應(yīng)用場景做簡單介紹。
應(yīng)用場景:
假設(shè)一個共享的文件米母,其屬性是可讀勾扭,如果某個時間有100個線程在同時讀取該文件,如果通過synchronized或者Lock來實現(xiàn)線程的同步訪問铁瞒,那么有個問題來了妙色,當(dāng)這100個線程的某個線程獲取到了鎖后,其它的線程都要等該線程釋放了鎖才能進行讀操作慧耍,這樣就會造成系統(tǒng)資源和時間極大的浪費身辨,而ReentrantReadWriteLock正好解決了這個問題。下面給一個簡單的例子芍碧,并根據(jù)代碼以及輸出結(jié)果做簡要說明:
public classTest{
public static voidmain(String[] args){
MyRunnable myRunnable = newTest().new MyRunnable();
Thread thread1 = new Thread(myRunnable,"thread1");
Thread thread2 = new Thread(myRunnable,"thread2");
Thread thread3 = new Thread(myRunnable,"thread3");
thread1.start();
thread2.start();
thread3.start();
}
public class MyRunnable implementsRunnable{
private ReadLocklock =new ReentrantReadWriteLock().readLock();
@Override
public synchronized void run() {
try{
lock.lock();
inti=0;
while(i<5) {
System.out.println(Thread.currentThread().getName() +"正在進行讀操作");
i++;
}
System.out.println(Thread.currentThread().getName()+"讀操作完畢");
}
finally{
lock.unlock();
}
}
}
}
輸出結(jié)果:
thread1正在進行讀操作
thread1正在進行讀操作
thread1正在進行讀操作
thread1正在進行讀操作
thread1正在進行讀操作
thread1讀操作完畢
thread3正在進行讀操作
thread3正在進行讀操作
thread3正在進行讀操作
thread3正在進行讀操作
thread3正在進行讀操作
thread3讀操作完畢
thread2正在進行讀操作
thread2正在進行讀操作
thread2正在進行讀操作
thread2正在進行讀操作
thread2正在進行讀操作
thread2讀操作完畢
從輸出結(jié)果可以看出煌珊,三個線程并沒有交替輸出,這是因為這里只是讀取了5次泌豆,但將讀取次數(shù)i的值改成一個較大的數(shù)值如100000時定庵,輸出結(jié)果就會交替的出現(xiàn)。
Lock與synchronized的比較
1)Lock是一個接口踪危,而synchronized是Java中的關(guān)鍵字蔬浙,synchronized是內(nèi)置的語言實現(xiàn);
2)synchronized在發(fā)生異常時贞远,會自動釋放線程占有的鎖畴博,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生。Lock在發(fā)生異常時蓝仲,如果沒有主動通過unLock()方法去釋放鎖俱病,則很可能造成死鎖的現(xiàn)象蜜唾,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應(yīng)中斷庶艾,而synchronized卻不行袁余,使用synchronized時,等待的線程會一直等待下去咱揍,不能夠響應(yīng)中斷颖榜;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到煤裙;
5)Lock可以提高多個線程進行讀操作的效率掩完。
Lock和synchronized比較主要是參考Java Lock和synchronized的區(qū)別。
volatile
當(dāng)一個變量被定義成volatile類型之后硼砰,該變量對所有的線程是可見的且蓬,這里的可見性指的是當(dāng)一個線程修改了該變量的值,那么新值對其它線程來說是立即可知的题翰。
雖然被volatile修飾的變量具有可見性恶阴,但是基于volatile變量的運算在并發(fā)下并不是安全的,因為Java里面的有些運算并非原子操作豹障,下面舉例說明:
public static volatile int index = 0;
public static void main(String[] args){
for(int i = 0; i < 3; i ++ ){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++){
index ++;
}
}
});
thread.start();
increase();
}
System.out.println("index:" + index);
}
輸出結(jié)果:
index:20
上面的代碼冯事,我們直觀的反應(yīng),輸出的index應(yīng)該是30血公,而不應(yīng)該是20昵仅。
其實不然,++操作不是一個原子操作累魔,index++指令編譯成字節(jié)碼包含兩個操作:取值摔笤,加操作。這里index被修飾成volatile垦写,保證了此時從內(nèi)存中取得的index值是正確的吕世,但有可能其它線程通過加操作將index值加1,之前從內(nèi)存中取得的index值過期了梯澜,這時候我們執(zhí)行加1操作后將一個較小的index值同步到內(nèi)存中寞冯。
volatile除了讓變量具有可見性外渴析,還有一個更為重要的語義:禁止指令重排優(yōu)化晚伙,保證變量的賦值操作跟程序代碼中的執(zhí)行順序是一致的。JVM往往會對代碼進行優(yōu)化俭茧,這些優(yōu)化操作可能會造成程序指令在執(zhí)行時會出現(xiàn)亂序咆疗,而volatile能夠屏蔽掉JVM中必要的代碼優(yōu)化。
總結(jié)
很多東西覺得不用筆系統(tǒng)地寫下來母债,過段時間找回就會花一定的時間午磁,于是就寫在簡書上尝抖。同時也歡迎大家指正(其中有些部分是引用了其它的文章,大家也可以做下參考)迅皇。