多執行緒的理解思路梳理 + synchronized與Lock 的比較
多執行緒的理解思路梳理 + synchronized與Lock 的比較
理解多執行緒的根本:資源問題與鎖的物件
synchronized與Lock解決的是多執行緒訪問資源時的執行緒安全問題,那麼這個存線上程安全問題的多執行緒模型中,一定滿足一個條件:多個執行緒訪問一個共享資源。
通常的實現模型是,自定義一個資源類,類中儲存具體資源,以及定義資源訪問的方法,這些方法就是需要提供執行緒安全的。
synchronized的理解
這時使用synchronized有三種實現方式:
- synchronized宣告普通方法:呼叫將取得當前類物件的鎖
- synchronized宣告靜態方法:呼叫將取得當前類的Class的鎖
- synchronized宣告程式碼塊:進入時取得括號宣告的物件的鎖
普通方法和靜態方法的說明:普通和靜態都可以實現資源的互斥訪問,只是一般當資源是靜態變數時,靜態的同步方法才有意義。特別注意普通方法取得的是物件鎖,靜態方法取得的是類鎖,物件鎖和類鎖是不互斥的。
同步方法和同步程式碼塊的說明:當一個資源訪問方法並不是所有程式碼都需要進行同步,一部分程式碼可以先行執行的時候,可以用synchronized程式碼塊代替方法宣告。注意程式碼塊鎖定的是小括號中宣告的物件,取得的仍然是物件鎖,當最先進入同步程式碼塊的執行緒取得了這個資源類物件的鎖後,其他的執行緒就在程式碼塊外阻塞等待鎖的釋放。儘量使用同步程式碼塊代替同步方法可以縮小鎖的粒度。
附1:生產者消費者實現程式碼
/*
* synchronized同步方法在資源類Property中,意味著produce和consume方法鎖定的是呼叫這個方法的Property物件,所以無論是多個生產者和消費者,還是一個生產者和一個消費者,只要啟動了多個執行緒就能實現效果
*/
class Property<T>{
private static final int MAX_LENGTH = 5;
private String properties[];
private int index = 0;//貨物索引
public Property() {
this.properties = new String[MAX_LENGTH];
}
public synchronized void produce(String str) {
if(this.index==5) {
System.out.println("我是生產者:"+Thread.currentThread().getName()+",產品已滿,我無法生產,馬上釋放鎖");
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
properties[index++] = str;
System.out.println("[生產者:"+Thread.currentThread().getName()+"]生產了產品");
super.notify();
}
}
public synchronized void consume() {
if(this.index==0) {
System.out.println("我是消費者:"+Thread.currentThread().getName()+",沒有產品,我無法消費,馬上釋放鎖");
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[消費者:"+Thread.currentThread().getName()+"]消費了產品:"+this.properties[--this.index]);
super.notify();
}
}
}
class Producer implements Runnable{
private Property pro;
public Producer(Property pro) {
this.pro = pro;
}
@Override
public void run() {
for(int x=0;x<10;x++) {
this.pro.produce("["+Thread.currentThread().getName()+"]"+"的生產品");
}
}
}
class Consumer implements Runnable{
private Property pro;
public Consumer(Property pro) {
this.pro = pro;
}
@Override
public void run() {
for(int x=0;x<10;x++) {
this.pro.consume();
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Property pro = new Property();
Producer p = new Producer(pro);
Consumer c = new Consumer(pro);
for(int x=1;x<=5;x++) {
new Thread(p,String.valueOf(x)).start();
}
for(int x=1;x<=5;x++) {
new Thread(c,String.valueOf(x)).start();
}
}
}
理解Thread和Runnable的區別
Runnable介面解決的不僅僅是Thread類的單繼承侷限,在有些情況下,使用繼承Thread類的方式無法滿足同步要求。當可以單獨定義資源類時,將資源類的物件設為執行緒實現類(如定義MyThread extends Thread)的成員,無論使用Thread還是Runnbale,只要main方法中綁定了同一資源類物件,就可以實現同步訪問;但當沒有這個所謂的資源類,僅僅希望實現同步方法時,這時synchronized的作用只能移到run()方法上,這意味著繼承Thread時,每次new例項化一個新的執行緒物件,synchronized進入run()時取得的是各個不同的執行緒物件的鎖,是無法同步的,而Runnable實現就不同,可以通過先例項化一個Runnable實現類,用這一個Runnable物件去建立多個執行緒,這樣所有執行緒嘗試取得的就是這個Runnable物件的鎖,所以synchronized宣告run()方法仍然可以實現同步
附2:Thread和Runnable都可以進行資源同步控制
class Property{
public synchronized void fun() {
//同步操作
}
}
class MyThread extends Thread{
Property pro;//資源繫結線上程實現類內部
public MyThread(Property pro) {
this.pro = pro;
}
@Override
public void run() {
// 資源操作
}
}
class MyRunnable implements Runnable{
Property pro;//資源繫結線上程實現類內部
public MyRunnable(Property pro) {
this.pro = pro;
}
public void run() {
// 資源操作
}
}
public class PropertiesTest {
public static void main(String[] args) {
Property pro = new Property();//用同一個資源類物件構造執行緒物件
Thread t1 = new MyThread(pro);
Thread t2 = new MyThread(pro);
t1.start();
t2.start();
Thread r1 = new Thread(new MyRunnable(pro));
Thread r2 = new Thread(new MyRunnable(pro));
r1.start();
r2.start();
}
}
附3:Thread無法實現單純的方法同步
class MyRunnable implements Runnable {
@Override
public synchronized void run() {
for(int x=0;x<100;x++){
System.out.println(Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
@Override
public synchronized void run() {
for(int x=0;x<100;x++){
System.out.println(Thread.currentThread().getName());
}
}
}
public class RunTest{
public static void main(String[] args) throws InterruptedException {
/*for(int x=0;x<3;x++){
Thread t = new Thread(new MyRunnable(),String.valueOf(x));
t.start();
}*/
for(int x=3;x<6;x++){
Thread t = new MyThread();
t.setName(String.valueOf(x));
t.start();
}
}
}
擷取部分結果:沒有實現同步
最後總結synchronized和Lock的區別
效能不一致:資源競爭激烈的情況下,Lock效能會比synchronized好,競爭不激烈的情況下,synchronized比Lock效能好。但jdk1.6後synchronized也加入了CAS的優化,效能大大提升。
一般認為synchronized關鍵字的實現是源自像訊號量之類的執行緒同步機制,在高併發狀態下,CPU消耗過多的時間線上程的排程上,從而造成了效能的極大浪費;Lock實現原理則是依賴於硬體,現代處理器都支援CAS指令。
機制不一致:synchronized是在JVM層面實現的,系統會監控鎖的釋放與否,無法手動中斷阻塞執行緒;Lock是基於程式碼實現的,可以非阻塞地獲取鎖,需要手動釋放,推薦在finally塊中釋放。
用法不一樣:synchronized可以用在程式碼塊上,方法上;Lock通過程式碼實現,有更精確的執行緒語義。synchronized只能以一個條件進行執行緒的休眠(wait)和喚醒(notify)控制;Lock可以獲取多個條件(Condition)進行更細緻的執行緒休眠(await)和喚醒(signal)的控制
(文章內容有過多方參考,來源過廣,不一一列出)