做者 某人Valar
如需轉載請保留原文連接html
相關推薦:java
目錄:bash
synchronized中文意爲:同步的,同步化的。是Java中的一個關鍵字。多線程
經常使用做給方法或者代碼塊加鎖。加鎖後,同一時刻只能有一個線程執行這段代碼。以此來保證線程安全。併發
先簡單理解下3個概念,jvm
原子(atom)本意指化學反應不可再分的基本微粒。
在編程中原子性指一個操做不可再被分隔成多步。一個操做或者多個操做 要麼所有執行且執行的過程不會被任何因素打斷,要麼就都不執行。爲何程序的執行順序有時會不按照代碼的前後順序執行呢?ide
這裏面涉及到指令重排序(Instruction Reorder)的概念。在Java內存模型中,容許編譯器和處理器對指令進行重排序,重排序的結果不會影響到單線程的執行,但不能保證多線程併發執行時不受影響。post
例如如下代碼在未發生指令重排序時,其執行順序爲1->2->3->4。但在真正執行時,將可能變爲1->2->4->3或者2->1->3->4或者其餘。但其會保證1處於3以前,2處於4以前。全部最終結果都是
a=10; b=20
。int a = 0;//語句1 int b = 1;//語句2 a = 10; //語句3 b = 20; //語句4 複製代碼
但若是是多線程狀況下,另外一個線程中有如下程序。當上述的執行順序被重排序爲1->2->4->3,當線程1執行到第3步
b=20
時,切換到線程2執行,其會輸出a此時已是10了
,而此時a的值其實仍是爲0。if(b == 20){ System.out.print("a此時已是10了"); } 複製代碼
被synchronized關鍵字包裹起來的方法或者代碼塊能夠認爲是原子的。由於在鎖未釋放以前,這段代碼沒法被其餘線程訪問到,因此從一個線程觀察另一個線程的時候,看到的都是一個個原子性的操做。
在Java中,synchronized對應着兩個字節碼指令
monitorenter
和monitorexit
。經過monitorenter
和monitorexit
指令,能夠保證被synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放以前,沒法被其餘線程訪問到。
根據JMM(Java Memory Model,Java內存模型)機制,內存主要分爲主內存和工做內存兩種,線程工做時會從主內存中拷貝一份變量到工做內存中。
JMM對synchronized作了2條規定:
synchronized能夠保證必定程度的有序性,但其是不能禁止指令重排序的,synchronized 代碼塊裏的非原子操做依舊可能發生指令重排。
具體怎麼理解呢?
as-if-serial語義
,其是指無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果都不能被改變。編譯器和處理器不管如何優化,都必須遵照as-if-serial語義
。as-if-serial語義
保證了單線程中指令重排序是有一些限制的,即不管怎麼重排序,都不能影響到單線程執行的結果。而synchronized保證了這一塊程序在同一時間內只能被同一線程訪問,因此其也算是保證了有序性。synchronized的用法大概能夠分爲3種,
其做用在方法上的寫法以下圖,synchronized
只要放在返回類型
前面就行。
下面將舉一些具體的實例,來看下使用synchronized
後的結果:
public class TestBean {
//TestBean中有兩個實例方法,method1和method2
public synchronized void method1(){
System.out.println("method1 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method1 end");
}
public synchronized void method2(){
System.out.println("method2 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method2 end");
}
}
複製代碼
public class MainTest {
public static void main(String[] args){
TestBean testBean = new TestBean();
//第一個線程,執行method1()
new Thread(new Runnable() {
@Override
public void run() {
testBean.method1();
}
}).start();
/馬上開啓第二個線程,執行method2()
new Thread(new Runnable() {
@Override
public void run() {
testBean.method2();
}
}).start();
}
}
複製代碼
控制檯結果:
method1 start
(...3秒後輸出)
method1 end
method2 start
(...3秒後輸出)
method2 end
複製代碼
能夠看出鎖做用於testBean
對象上,其餘線程來訪問synchronized
修飾的其餘方法時須要等待線程1先把鎖釋放。
method2()
的synchronized
修飾符去掉呢。那天然是不用等鎖釋放,就會馬上執行method2()
。控制檯輸出如下結果:method1 start
method2 start
(...3秒後輸出)
method1 end
method2 end
複製代碼
synchronized
修飾,但有兩個不一樣的實例對象。public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
//第一個線程,執行method1()
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method1();
}
}).start();
/馬上開啓第二個線程,執行method2()
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method2();
}
}).start();
}
}
複製代碼
此時由於兩個線程做用於不一樣的對象,得到的是不一樣的鎖,因此互相併不影響,控制檯的結果以下:
method1 start
method2 start
(...3秒後輸出)
method1 end
method2 end
複製代碼
將上文中的TestBean.java改成:
public class TestBean {
//TestBean中有一個靜態方法,method
synchronized public static void method(String threadName){
System.out.println("method start by " + threadName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method end by " + threadName);
}
}
複製代碼
Main.java以下:
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method("thread1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method("thread2");
}
}).start();
}
}
複製代碼
控制檯結果:
method start by thread1
(...3秒後輸出)
method end by thread1
method start by thread2
(...3秒後輸出)
method end by thread2
複製代碼
分析:由例子可知,兩個線程雖然使用的是兩個不一樣的對象,可是訪問的方法是靜態的,兩個線程最終仍是發生了互斥(即一個線程訪問,另外一個線程只能等着),由於靜態方法是依附於類而不是對象的,當synchronized修飾靜態方法時,鎖是class對象。
爲何要做用於代碼塊呢?
在某些狀況下,咱們編寫的方法體可能比較大,同時存在一些比較耗時的操做,而須要同步的代碼又只有一小部分,此時咱們可使用同步代碼塊的方式對須要同步的代碼進行包裹,畢竟長鎖不如短鎖,儘量只鎖必要的部分。
public class TestBean {
private final static Object objectLock = new Object();
void method1(){
System.out.println("not synchronized method1");
synchronized (objectLock){
System.out.println("synchronized method1 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized method1 end ");
}
}
void method2(){
System.out.println("not synchronized method2" );
synchronized (objectLock){
System.out.println("synchronized method2 start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized method2 end");
}
}
}
複製代碼
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
TestBean testBean2 = new TestBean();
new Thread(new Runnable() {
@Override
public void run() {
testBean1.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
testBean2.method2();
}
}).start();
}
}
複製代碼
控制檯輸出:
not synchronized method1
synchronized method1 start
not synchronized method2
(...3秒後輸出)
synchronized method1 end
synchronized method2 start
(...3秒後輸出)
synchronized method2 end
複製代碼
能夠看到未被synchronized包裹的代碼時不存在互斥的,System.out.println("not synchronized method2" );
無需等到method1執行完成,而被synchronized包裹的代碼塊,且使用了同一個對象做爲鎖的話,那就互斥了。
結論:
synchronized
做用於一個給定的實例對象objectLock,每次當線程進入synchronized
包裹的代碼塊時就會要求當前線程持有objectLock實例對象鎖,若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待。this
對象(表明當前實例)或者當前類的class對象
做爲鎖,以下代碼:synchronized (this){
System.out.println("synchronized method1 start");
System.out.println("synchronized method1 end");
}
synchronized (TestBean.class){
System.out.println("synchronized method2 start");
System.out.println("synchronized method2 end");
}
複製代碼
Lock
是Java語言中的一個接口類,對應的實現類爲ReentrantLock
,它們都位於java.util.concurrent.locks
包下,熟悉Java的同窗確定都知道concurrent包下都是用於處理Java多線程問題的類。
lock的幾個經常使用方法:
lock():獲取鎖,若是鎖被暫用則一直等待
unlock():釋放鎖
tryLock(): 注意返回類型是boolean,若是獲取鎖的時候鎖被佔用就返回false,不然返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
ck
lockInterruptibly():用該鎖的得到方式,若是線程在獲取鎖的階段進入了等待,那麼能夠中斷此線程,先去作別的事
複製代碼
lock的使用與synchronized做用於代碼塊時相似:
public class TestBean {
private Lock lock = new ReentrantLock();
void method1(String threadName) {
lock.lock();
try{
System.out.println("method1 start " + threadName);
//耗時操做
...
} finally {
System.out.println("method1 end " + threadName);
lock.unlock();//釋放鎖
}
}
}
複製代碼
public class MainTest {
public static void main(String[] args){
TestBean testBean1 = new TestBean();
new Thread("thread1") {
@Override
public void run() {
testBean1.method1(Thread.currentThread().getName());
}
}.start();
new Thread("thread2"){
@Override
public void run() {
testBean1.method1(Thread.currentThread().getName());
}
}.start();
}
}
複製代碼
執行結果以下:
method1 start thread1
(...3秒後輸出)
method1 end thread1
method1 start thread2
(...3秒後輸出)
method1 end thread2
複製代碼
注意:使用lock時,須要在finally中釋放鎖lock.unlock();
,否則可能會形成死鎖。
tryLock()
方法的使用:public class TestBean {
private Lock lock = new ReentrantLock();
void method1(String threadName) {
if (lock.tryLock()){
try{
System.out.println("method1 start " + threadName);
//耗時操做
...
} finally {
lock.unlock();
}
} else {
System.out.println("我是"+threadName+",有人佔着鎖,我放棄了");
}
}
}
複製代碼
控制檯輸出:
method1 start thread1
我是thread2,有人佔着鎖,我放棄了
(...3秒後輸出)
method1 end thread1
複製代碼
類別 | synchorinzed | lock |
---|---|---|
存在層次 | Java的關鍵字 | 接口類,由ReentrantLock實現 |
鎖的釋放 | 一、以獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖 | 在finally中必須釋放鎖,否則容易形成線程死鎖 |
鎖的獲取 | 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 | 分狀況而定,Lock有多個鎖獲取的方式,可嘗試得到鎖tryLock() ,線程能夠不用一直等待 |
鎖的狀態 | 沒法判斷 | 可判斷 |
性能 | 資源競爭不是很激烈的狀況下,比較合適的;當同步很是激烈的時候,synchronized的性能會一會兒能降低得很快 | 在資源競爭不激烈的情形下,性能稍微比synchronized差點;但其在資源競爭激烈時,可維持常態 |
二者具體的性能測試:www.cnblogs.com/nsw2018/p/5…
以後會寫一篇文章描述下synchronizeds的底層原理: 涉及到的概念會有Monitor、monitorenter和monitorexit指令、synchronized的可重入性、synchronized與中斷。
另外會寫一篇關於volatile的文章:主要涉及Volatile的特性、其實現原理、指令重排序與內存屏障