您好,登錄后才能下訂單哦!
先來一道并發編程筆試題
題目:利用5個線程并發執行,num數字累計計數到10000,并打印。
/**
* Description:
* 利用5個線程并發執行,num數字累加計數到10000,并打印。
* 2019-06-13
* Created with OKevin.
*/
public class Count {
private int num = 0;
public static void main(String[] args) throws InterruptedException {
Count count = new Count();
Thread thread1 = new Thread(count.new MyThread());
Thread thread2 = new Thread(count.new MyThread());
Thread thread3 = new Thread(count.new MyThread());
Thread thread4 = new Thread(count.new MyThread());
Thread thread5 = new Thread(count.new MyThread());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
System.out.println(count.num);
}
private synchronized void increse() {
for (int i = 0; i < 2000; i++) {
num++;
}
}
class MyThread implements Runnable {
@Override
public void run() {
increse();
}
}
}
這道并發編程面試題,題目不難,方法簡單。其中涉及一個核心知識點——synchronized(當然這題的解法有很多),這也是本文想要弄清的主題。
synchronized被大大小小的程序員廣泛使用,有的程序員偷懶,在要求保證線程安全時,不加思索的就在方法前加入了synchronized關鍵字(例如我剛才那道大題)。偷懶歸偷懶,CodeReview總是要進行的,面對同事的“指責”,要求優化這個方法,將synchronized使用同步代碼塊的方式提高效率。
synchronized要按照同步代碼塊來保證線程安全,這可就加在方法“復雜”多了。有:synchronized(this){}這么寫的,也有synchronized(Count.class){}這么寫的,還有定義了一個private Object obj = new Object; ….synchronized(obj){}這么寫的。此時不禁在心里“W*F”。
synchronized你到底鎖住的是誰?
synchronized從語法的維度一共有3個用法:
前兩種方式最為偷懶,第三種方式比前兩種性能要好。
synchronized從鎖的是誰的維度一共有兩種情況:
我們還是從直觀的語法結構上來講述synchronized。
靜態方法是屬于“類”,不屬于某個實例,是所有對象實例所共享的方法。也就是說如果在靜態方法上加入synchronized,那么它獲取的就是這個類的鎖,鎖住的就是這個類。
實例方法并不是類所獨有的,每個對象實例獨立擁有它,它并不被對象實例所共享。這也比較能推出,在實例方法上加入synchronized,那么它獲取的就是這個累的鎖,鎖住的就是這個對象實例。
那鎖住類還是鎖住對象實例,這跟我線程安全關系大嗎?大,差之毫厘謬以千里的大。為了更好的理解鎖住類還是鎖住對象實例,在進入“3)方法中使用同步代碼塊”前,先直觀的感受下這兩者的區別。
首先定義一個Demo類,其中的實例方法加上了synchronized關鍵字,按照所述也就是說鎖住的對象實例。
/**
* Description:
* 死循環,目的是兩個線程搶占一個鎖時,只要其中一個線程獲取,另一個線程就會一直阻塞
* 2019-06-13
* Created with OKevin.
*/
public class Demo {
public synchronized void demo() {
while (true) { //synchronized方法內部是一個死循環,一旦一個線程持有過后就不會釋放這個鎖
System.out.println(Thread.currentThread());
}
}
}
可以看到在demo方法中定義了一個死循環,一旦一個線程持有這個鎖后其他線程就不可能獲取這個鎖。結合上述synchronized修飾實例方法鎖住的是對象實例,如果兩個線程針對的是一個對象實例,那么其中一個線程必然不可能獲取這個鎖;如果兩個線程針對的是兩個對象實例,那么這兩個線程不相關均能獲取這個鎖。
自定義線程,調用demo方法。
/**
* Description:
* 自定義線程
* 2019-06-13
* Created with OKevin.
*/
public class MyThread implements Runnable {
private Demo demo;
public MyThread(Demo demo) {
this.demo = demo;
}
@Override
public void run() {
demo.demo();
}
}
測試程序1:兩個線程搶占一個對象實例的鎖
/**
* Description:
* 兩個線程搶占一個對象實例的鎖
* 2019-06-13
* Created with OKevin.
*/
public class Main1 {
public static void main(String[] args) {
Demo demo = new Demo();
Thread thread1 = new Thread(new MyThread(demo));
Thread thread2 = new Thread(new MyThread(demo));
thread1.start();
thread2.start();
}
}
?如上圖所示,輸出結果顯然只會打印一個線程的信息,另一個線程永遠也獲取不到這個鎖。
測試程序2:兩個線程分別搶占兩個對象實例的鎖
/**
* Description:
* 兩個線程分別搶占兩個對象實例的鎖
* 2019-06-13
* Created with OKevin.
*/
public class Main2 {
public static void main(String[] args) {
Demo demo1 = new Demo();
Demo demo2 = new Demo();
Thread thread1 = new Thread(new MyThread(demo1));
Thread thread2 = new Thread(new MyThread(demo2));
thread1.start();
thread2.start();
}
}
如上圖所示,顯然,兩個線程均進入到了demo方法,也就是均獲取到了鎖,證明,兩個線程搶占的就不是同一個鎖,這就是synchronized修飾實例方法時,鎖住的是對象實例的解釋。
靜態方法是類所有對象實例所共享的,無論定義多少個實例,是要是靜態方法上的鎖,它至始至終只有1個。將上面的程序Demo中的方法加上static,無論使用“測試程序1”還是“測試程序2”,均只有一個線程可以搶占到鎖,另一個線程仍然是永遠無法獲取到鎖。
讓我們重新回到從語法結構上解釋synchronized。
程序的改良優化需要建立在有堅實的基礎,如果在不了解其內部機制,改良也僅僅是“形式主義”。
結合開始CodeReview的例子:
你的同事在CodeReview時,要求你將實例方法上的synchronized,改為效率更高的同步代碼塊方式。在你不清楚同步代碼的用法時,網上搜到了一段synchronized(this){}代碼,復制下來發現也能用,此時你以為你改良優化了代碼。但實際上,你可能只是做了一點形式主義上的優化。
為什么這么說?這需要清楚地認識同步代碼塊到底應該怎么用。
this關鍵字所代表的意思是該對象實例,換句話說,這種用法synchronized鎖住的仍然是對象實例,他和public synchronized void demo(){}可以說僅僅是做了語法上的改變。?
/**
* 2019-06-13
* Created with OKevin.
**/
public class Demo {
public synchronized void demo1() {
while (true) { //死循環目的是為了讓線程一直持有該鎖
System.out.println(Thread.currentThread());
}
}
public synchronized void demo2() {
while (true) {
System.out.println(Thread.currentThread());
}
}
}
改為以下方式:?
/**
* Description:
* synchronized同步代碼塊對本實例加鎖(this)
* 假設demo1與demo2方法不相關,此時兩個線程對同一個對象實例分別調用demo1與demo2,只要其中一個線程獲取到了鎖即執行了demo1或者demo2,此時另一個線程會永遠處于阻塞狀態
* 2019-06-13
* Created with OKevin.
*/
public class Demo {
public void demo1() {
synchronized (this) {
while (true) { //死循環目的是為了讓線程一直持有該鎖
System.out.println(Thread.currentThread());
}
}
}
public void demo2() {
synchronized (this) {
while (true) {
System.out.println(Thread.currentThread());
}
}
}
}
也許后者在JVM中可能會做一些特殊的優化,但從代碼分析上來講,兩者并沒有做到很大的優化,線程1執行demo1,線程2執行demo2,由于兩個方法均是搶占對象實例的鎖,只要有一個線程獲取到鎖,另外一個線程只能阻塞等待,即使兩個方法不相關。
/**
* Description:
* synchronized同步代碼塊對對象內部的實例加鎖
* 假設demo1與demo2方法不相關,此時兩個線程對同一個對象實例分別調用demo1與demo2,均能獲取各自的鎖
* 2019-06-13
* Created with OKevin.
*/
public class Demo {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void demo1() {
synchronized (lock1) {
while (true) { //死循環目的是為了讓線程一直持有該鎖
System.out.println(Thread.currentThread());
}
}
}
public void demo2() {
synchronized (lock2) {
while (true) {
System.out.println(Thread.currentThread());
}
}
}
}
經過上面的分析,看到這里,你可能會開始懂了,可以看到demo1方法中的同步代碼塊鎖住的是lock1對象實例,demo2方法中的同步代碼塊鎖住的是lock2對象實例。如果線程1執行demo1,線程2執行demo2,由于兩個方法搶占的是不同的對象實例鎖,也就是說兩個線程均能獲取到鎖執行各自的方法(當然前提是兩個方法互不相關,才不會出現邏輯錯誤)。
這種形式等同于搶占獲取類鎖,這種方式,同樣和3.1一樣,收效甚微。
所以CodeReivew后的代碼應該是3.2) private Object obj = new Object(); ???synchronized(obj){...},這才是對你代碼的改良優化。
本文的重點是你有沒有收獲與成長,其余的都不重要,希望讀者們能謹記這一點。同時我經過多年的收藏目前也算收集到了一套完整的學習資料,包括但不限于:分布式架構、高可擴展、高性能、高并發、Jvm性能調優、Spring,MyBatis,Nginx源碼分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多個知識點高級進階干貨,希望對想成為架構師的朋友有一定的參考和幫助
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。