水滴石穿--多執行緒安全同步與鎖
什麼是執行緒安全?
當多個執行緒訪問某一個類(物件或方法)時,這個物件始終都能表現出正確的行為, 那麼這個類(物件或方法)就是執行緒安全的。
java記憶體模型
在說明多執行緒安全問題前,要明白java記憶體模型。
為什麼有執行緒安全問題?
當多個執行緒同時共享,同一個全域性變數或靜態變數,做寫的操作時,可能會發生資料衝突問題,也就是執行緒安全問題。但是做讀操作是不會發生資料衝突問題。
經典售票問題我們可以一起寫一下!!
/** * @classDesc: 演示多執行緒的安全問題 * @author: hj * @date:2018年12月12日 上午10:49:59 */ public class ThreadTrain implements Runnable { private int trainCount = 100; @Override public void run() { synchronized (this) { while (trainCount > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } sale(); } } } // 模仿賣票 public void sale() { if (trainCount > 0) { System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票"); trainCount--; } } public static void main(String[] args) { ThreadTrain threadTrain = new ThreadTrain(); Thread t0 = new Thread(threadTrain, "t0"); Thread t1 = new Thread(threadTrain, "t1"); Thread t2 = new Thread(threadTrain, "t2"); t0.start(); t1.start(); t2.start(); } }
如果在run方法中不加入同步程式碼塊就會出現執行緒不安全的問題。
執行緒安全解決辦法:
問:如何解決多執行緒之間執行緒安全問題
答:使用多執行緒之間同步synchronized或使用鎖(lock)。
問:為什麼使用執行緒同步或使用鎖能解決執行緒安全問題呢?
答:將可能會發生資料衝突問題(執行緒不安全問題),只能讓當前一個執行緒進行執行。程式碼執行完成後釋放鎖,讓後才能讓其他執行緒進行執行。這樣的話就可以解決執行緒不安全問題。
問:什麼是多執行緒之間同步
答:當多個執行緒共享同一個資源,不會受到其他執行緒的干擾。
問:什麼是多執行緒同步
答:當多個執行緒共享同一個資源,不會受到其他執行緒的干擾。
下面先看看一個通過synchronized實現的執行緒安全例子
/** * 執行緒安全:當多個執行緒訪問某一個類(物件或方法)時,這個物件始終都能表現出正確的行為, 那麼這個類(物件或方法)就是執行緒安全的。 * synchronized:可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為"互斥區"或"臨界區" * * @author 4 Todo:演示執行緒安全與不安全,通過synchronized關鍵字控制 */ public class ThreadSafe extends Thread { private int count = 0; /* synchronized */ @Override public synchronized void run() { count++; System.out.println("thread: " + Thread.currentThread().getName() + " do: " + count); } public static void main(String[] args) { /** * 分析:當多個執行緒訪問myThread的run方法時,以排隊的方式進行處理(這裡排對是按照CPU分配的先後順序而定的), * 一個執行緒想要執行synchronized修飾的方法裡的程式碼: 1 嘗試獲得鎖 2 * 如果拿到鎖,執行synchronized程式碼體內容;拿不到鎖,這個執行緒就會不斷的嘗試獲得這把鎖,直到拿到為止, * 而且是多個執行緒同時去競爭這把鎖。(也就是會有鎖競爭的問題) */ ThreadSafe threadSafe = new ThreadSafe(); Thread t1 = new Thread(threadSafe, "t1"); Thread t2 = new Thread(threadSafe, "t2"); Thread t3 = new Thread(threadSafe, "t3"); Thread t4 = new Thread(threadSafe, "t4"); t1.start(); t2.start(); t3.start(); t4.start(); } }
也可以試著把synchronized去掉看看結果。
接下來我們看看java鎖的感念!
內建的鎖
Java提供了一種內建的鎖機制來支援原子性
每一個Java物件都可以用作一個實現同步的鎖,稱為內建鎖,執行緒進入同步程式碼塊之前自動獲取到鎖,程式碼塊執行完成正常退出或程式碼塊中丟擲異常退出時會釋放掉鎖
內建鎖為互斥鎖,即執行緒A獲取到鎖後,執行緒B阻塞直到執行緒A釋放鎖,執行緒B才能獲取到同一個鎖
內建鎖使用synchronized關鍵字實現,synchronized關鍵字有兩種用法:
1.修飾需要進行同步的方法(所有訪問狀態變數的方法都必須進行同步),此時充當鎖的物件為呼叫同步方法的物件
2.同步程式碼塊和直接使用synchronized修飾需要同步的方法是一樣的,但是鎖的粒度可以更細,並且充當鎖的物件不一定是this,也可以是其它物件,所以使用起來更加靈活
現在我們著重對synchronized做說明!
同步程式碼塊synchronized
就是將可能會發生執行緒安全問題的程式碼,給包括起來。 synchronized(同一個資料){ 可能會發生執行緒衝突問題 } 就是同步程式碼塊 synchronized(物件)//這個物件可以為任意物件 { 需要被同步的程式碼 } |
synchronized (this) {
while (trainCount > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
sale();
}
}
物件如同鎖,持有鎖的執行緒可以在同步中執行
沒持有鎖的執行緒即使獲取CPU的執行權,也進不去
同步的前提:
1,必須要有兩個或者兩個以上的執行緒
2,必須是多個執行緒使用同一個鎖
必須保證同步中只能有一個執行緒在執行
好處:解決了多執行緒的安全問題
弊端:多個執行緒需要判斷鎖,較為消耗資源、搶鎖的資源。
同步方法:
什麼是同步方法?
答:在方法上修飾synchronized 稱為同步方法
public synchronized void run() {
count++;
System.out.println("thread: " + Thread.currentThread().getName() + " do: " + count);
}
同步方法使用的是什麼鎖?
答:同步函式使用this鎖。
補充幾點瑣碎的知識點
1.原子操作(業務整體需要使用完整的synchronized,保持業務的原子性。)
/**
* 業務整體需要使用完整的synchronized,保持業務的原子性。
*/
public class DirtyRead {
private String name = "";
private int age = 0;
/* synchronized */
public synchronized void getvalue() {
System.out.println("name is:" + name + " age is:" + age);
}
public synchronized void setvalue(String name, int age) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.age = age;
System.out.println("修改完成 " + "name is:" + name + " age is:" + age);
}
public static void main(String[] args) {
DirtyRead dirtyRead = new DirtyRead();
new Thread(new Runnable() {
@Override
public void run() {
dirtyRead.setvalue("hj", 100);
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
dirtyRead.getvalue();
}
}
2.鎖的佔有問題(關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼(方法)當做鎖,所以程式碼中哪個執行緒先執行synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖(Lock),在靜態方法上加synchronized關鍵字,表示鎖定.class類,類一級別的鎖(獨佔.class類)。)
/**
* 關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼(方法)當做鎖,
* 所以程式碼中哪個執行緒先執行synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖(Lock),
* 在靜態方法上加synchronized關鍵字,表示鎖定.class類,類一級別的鎖(獨佔.class類)。
*
* @author 4
*
*/
public class MultiThread {
private static int num = 0;
/* static */
private synchronized void print(String tag) {
try {
if (tag.equals("a")) {
num = 100;
System.out.println("tag set a,num is: " + num);
Thread.sleep(2000);
} else if (tag.equals("b")) {
num = 200;
System.out.println("tag set b,num is: " + num);
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MultiThread m1 = new MultiThread();
new Thread(new Runnable() {
@Override
public void run() {
m1.print("a");
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
m1.print("b");
}
}, "t2").start();
}
}
3.物件鎖的同步和非同步問題(只有synchronized方法或者需要同步的程式碼才會同步執行,否則都是非同步的)
/* *
* 物件鎖的同步和非同步問題
* */
public class Myobject {
public synchronized void method1() {
try {
Thread.sleep(2000);
System.out.println("method1被執行了。。。。");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/** synchronized */
public void method2() {
System.out.println("method2被執行了。。。。");
}
public static void main(String[] args) {
Myobject objectlock = new Myobject();
new Thread(new Runnable() {
@Override
public void run() {
objectlock.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
objectlock.method2();
}
}).start();
}
}
4.synchronized的重入(1.synchronized的重入 2.synchronized通過繼承可以傳遞子類)
/**
* 1.synchronized的重入 <br>
* 2.synchronized通過繼承可以傳遞子類(省略不寫了)
*/
public class SyncDubbo1 {
public synchronized void method1() {
System.out.println("method1..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
method2();
}
public synchronized void method2() {
System.out.println("method2..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
method3();
}
public synchronized void method3() {
System.out.println("method3..");
}
public static void main(String[] args) {
SyncDubbo1 syncDubbo1=new SyncDubbo1();
new Thread(new Runnable() {
@Override
public void run() {
syncDubbo1.method1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
syncDubbo1.method2();
}
}).start();
}
}
5.鎖的粒度大小
這一塊本來是在lock這一塊才說明現在先提一句。鎖的粒度大小會影響執行的效率,粒度越大效率越差
LOCK使用
首先看看所得型別(物件鎖,類鎖,任意物件鎖)
/***
* 物件鎖,類鎖,任意物件鎖都可以枷鎖
* @author 4
*/
public class ObjectLock {
public void method1() {
synchronized (this) {// 物件鎖
try {
System.out.println("do method1");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void method2() {
synchronized (this.getClass()) {// 類鎖
try {
System.out.println("do method2");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Object objectlock = new Object();
public void method3() {
synchronized (objectlock) {// 任意物件鎖
try {
System.out.println("do method3");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ObjectLock objLock = new ObjectLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method3();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method3();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method2();
}
});
t1.start();
t2.start();
t3.start();
}
}
再說說所得粒度大小問題:
1.同步方法<同步程式碼塊:因為一個是進入方法內加鎖的,一個是在排隊方法外加鎖,這樣即使獲得了鎖,進入方法中還得分配資源需要一定的時間。
2.減小鎖的大小,byte陣列物件鎖,推薦的寫法,效率好。
private byte []lock=new byte [1]
3.減小同步程式碼塊的大小
/**
* 使用synchronized程式碼塊減小鎖的粒度,提高效能, synchronized包含的業務要小
*/
public class Optimize {
public void doLongTimeTask() {
try {
System.out.println("當前執行緒開始:" + Thread.currentThread().getName() + ", 正在執行一個較長時間的業務操作,其內容不需要同步");
Thread.sleep(2000);
synchronized (this) {
System.out.println("當前執行緒:" + Thread.currentThread().getName() + ", 執行同步程式碼塊,對其同步變數進行操作");
Thread.sleep(1000);
}
System.out.println("當前執行緒結束:" + Thread.currentThread().getName() + ", 執行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final Optimize otz = new Optimize();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
}, "t2");
t1.start();
t2.start();
}
}
鎖物件改變鎖失效
/*
* 鎖物件的改變,鎖失效6
* */
public class ChangeLock {
private String lock = new String("lock");
private void method() {
synchronized (lock) {
try {
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + "start.....");
// lock = new String("change lock");
Thread.sleep(2000);
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + "end.....");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChangeLock changeLock = new ChangeLock();
new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t2").start();
new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
}, "t3").start();
}
}
讀者可以放開註釋,體驗鎖失效的效果
ps:不要在鎖的程式碼塊中修改鎖的內容。
顯示鎖reentrantlock
reentrantlock主要有4種方法lock()ulock()newCondition() getHoldCount()
1.reentrantlock入門lock()ulock()方法
/**
* @classDesc:ReentrantLock lock unlock演示
* @author: hj
* @date:2018年12月12日 下午1:07:27
*/
public class UseReentrantLock {
public Lock lock = new ReentrantLock();
public void method1() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入method1..");
Thread.sleep(1000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "退出method1..");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void method2() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入method2..");
Thread.sleep(2000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "退出method2..");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantLock ur = new UseReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ur.method1();
ur.method2();
}
}, "t1");
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.reentrantlock的newCondition()方法
/**
* @classDesc:lock.newCondition()方法演示
* @author: hj
* @date:2018年12月12日 下午1:16:02
*/
public class UseCondition {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void method1() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入等待狀態..");
Thread.sleep(3000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "釋放鎖..");
condition.await(); // Object wait
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "繼續執行...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void method2() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入..");
Thread.sleep(3000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "發出喚醒..");
condition.signal(); // Object notify
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseCondition uc = new UseCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
uc.method2();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
uc.method2();
}
}, "t2");
t1.start();
t2.start();
}
}
3. reentrantlock的多個newCondition()方法
/**
* @classDesc: lock.newCondition()多condition的用法
* @author: hj
* @date:2018年12月12日 下午1:17:48
*/
public class UseManyCondition {
private ReentrantLock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
public void m1() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入方法m1等待..");
c1.await();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "方法m1繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m2() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入方法m2等待..");
c1.await();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "方法m2繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m3() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入方法m3等待..");
c2.await();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "方法m3繼續..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m4() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "喚醒..");
c1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m5() {
try {
lock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "喚醒..");
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseManyCondition umc = new UseManyCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
umc.m1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
umc.m2();
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
umc.m3();
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
umc.m4();
}
}, "t4");
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
umc.m5();
}
}, "t5");
t1.start(); // c1
t2.start(); // c1
t3.start(); // c2
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t4.start(); // c1
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t5.start(); // c2
}
}
4.holdCount鎖的持有次數
/**
* lock.getHoldCount()方法:只能在當前呼叫執行緒內部使用,不能再其他執行緒中使用
* 那麼我可以在m1方法裡去呼叫m2方法,同時m1方法和m2方法都持有lock鎖定即可 測試結果holdCount數遞增
*/
public class TestHoldCount {
// 重入鎖
private ReentrantLock lock = new ReentrantLock();
public void m1() {
try {
lock.lock();
System.out.println("進入m1方法,holdCount數為:" + lock.getHoldCount());
// 呼叫m2方法
m2();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m2() {
try {
lock.lock();
System.out.println("進入m2方法,holdCount數為:" + lock.getHoldCount());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
TestHoldCount thc = new TestHoldCount();
thc.m1();
thc.m2();
System.out.println("holdCount數為:" + thc.lock.getHoldCount());
}
}
顯示鎖readwritelock
主要記住:RR共享 WW、RW、WW排斥
/**
* @classDesc: 讀寫鎖的使用
* @author: hj
* @date:2018年12月12日 下午1:21:30
*/
public class UseReentrantReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ReadLock readLock = rwLock.readLock();
private WriteLock writeLock = rwLock.writeLock();
public void read() {
try {
readLock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入...");
Thread.sleep(3000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "退出...");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write() {
try {
writeLock.lock();
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "進入...");
Thread.sleep(3000);
System.out.println("當前執行緒:" + Thread.currentThread().getName() + "退出...");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
}, "t4");
t1.start(); // RR共享
t2.start();
// t1.start(); // R 互斥
// t3.start(); // W
// t3.start();//互斥
// t4.start();
}
}
多執行緒死鎖
什麼是多執行緒死鎖?
答:同步中巢狀同步,導致鎖無法釋放
/**
* @classDesc: 死鎖問題,在設計程式時就應該避免雙方相互持有對方的鎖的情況 演示死鎖
* @author: hj
* @date:2018年12月12日 下午12:13:01
*/
public class DeadLock implements Runnable {
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag) {
this.tag = tag;
}
@Override
public void run() {
if (tag.equals("a")) {
synchronized (lock1) {
try {
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + " 進入lock1執行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + " 進入lock2執行");
}
}
}
if (tag.equals("b")) {
synchronized (lock2) {
try {
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + " 進入lock2執行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("當前執行緒 : " + Thread.currentThread().getName() + " 進入lock1執行");
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
d1.setTag("a");
DeadLock d2 = new DeadLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
面試題
1.什麼是執行緒安全
2.java記憶體模型
3.執行緒安全實現
4.同步程式碼的優化
5.鎖的加鎖、釋放,以及讀寫鎖的使用特點
6.死鎖的概念