執行緒安全問題:
在多執行緒中,有可能出現多個執行緒同時使用同一個資源的情況,這個資源可以是變數,資料表,txt檔案等。這個資源稱作"臨界資源"
舉個例子:取錢這個執行緒分為三個步驟:1.讀取金額、2.取款、3.更新金額
有個典型的執行緒安全的例子,倘若A,B兩人使用同一個賬戶(1000元)取款,執行順序如下:
1.A讀取金額,顯示1000
2.A取款300元
3.B讀取金額,顯示為1000(實際應該為700)
4.B取款1000元
5.A更新金額為700
6.B更新金額0
之所以賬戶中莫名地被多取了300,是因為A和B兩個執行緒使用了同一個臨界資源(賬戶)
注意:當多個執行緒執行同一個方法時,方法內部的變數不是臨界資源,因為每個執行緒都有自己獨立的記憶體區域(PC,方法棧,執行緒棧)
複製程式碼
解決執行緒安全問題:
基本所有的併發方案,都採用“序列化訪問資源”,也就是在同一時間,只有一個執行緒能訪問臨界資源,也稱作同步互斥訪問
方案1:synchronized
在Java中,每個物件都有一個鎖標記,稱為monitor(監視器),多個執行緒訪問這個物件的臨界資源時,只有獲取了該物件的鎖才能訪問
在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。
複製程式碼
public class ThreadSynchronized2 {
public static void main(String[] agrs) {
InsertData insertData = new InsertData();
new Thread("執行緒1") {
@Override
public void run() {
insertData.insert(Thread.currentThread());
}
}.start();
new Thread("執行緒2") {
@Override
public void run() {
insertData.insert(Thread.currentThread());
}
}.start();
}
}
class InsertData extends Thread {
public synchronized void insert(Thread thread) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
複製程式碼
輸出:
執行緒1插入: 0
執行緒1插入: 1
執行緒1插入: 2
執行緒1插入: 3
執行緒1插入: 4
執行緒2插入: 0
執行緒2插入: 1
執行緒2插入: 2
執行緒2插入: 3
執行緒2插入: 4
複製程式碼
還可以改成以下兩種方式:
class InsertData extends Thread {
public void insert(Thread thread) {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
}
複製程式碼
class InsertData extends Thread {
private Object object = new Object();
public void insert(Thread thread) {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
}
複製程式碼
說明:
1)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒不能訪問該物件的其他synchronized方法。這個原因很簡單,因為一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized方法。
2)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒能訪問該物件的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該物件的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他執行緒是可以訪問這個方法的,
3)如果一個執行緒A需要訪問物件object1的synchronized方法fun1,另外一個執行緒B需要訪問物件object2的synchronized方法fun1,即使object1和object2是同一型別的物件,也不會產生執行緒安全問題,因為他們訪問的是不同的物件,所以不存在互斥問題。
複製程式碼
注意事項:
static方法,使用的是類鎖(多個物件使用同一個類的monitor)
和非static方法,使用的是物件鎖(每個物件維護一個monitor)
複製程式碼
synchronized的優缺點:
1.使用synchronized包住的程式碼塊,只可能有兩種狀態:順利執行完畢釋放鎖、執行時發生異常釋放鎖,不會由於異常導致出現死鎖現象
2.如果synchronized包住的程式碼塊中有sleep等操作,比如I/O阻塞,但是其他執行緒還是需要等待,這樣程式的效率就比較低了
3.等待synchronized釋放鎖的執行緒會一直等待下去(死心塌地,不到黃河心不死)
複製程式碼
舉例場景:
對a.txt這個檔案,A,B執行緒都可以進行讀寫,寫操作與寫操作會發生衝突,寫操作與讀操作會發生衝突,但是,讀操作與讀操作不會發生衝突
這個時候synchronized滿足不了需求,就需要用高階的鎖
複製程式碼
鎖的介紹:
先看JDK中對於Lock的定義:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
複製程式碼
通過Lock的原始碼可以知道,lock(),tryLock(),tryLock(long time,Time Unit),lockInterruptiably()是用來獲取鎖的
lock()方法是平常使用得最多的一個方法,用來獲取鎖,如果鎖已經被其他執行緒獲取,則進行等待,可以說和synchronized沒有差別
複製程式碼
Lock lock = ...;
lock.lock();
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
複製程式碼
實際例子:
public class ThreadLock {
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ThreadLock mt = new ThreadLock();
new Thread(() -> mt.insert()).start();
new Thread(() -> mt.insert()).start();
}
public void insert() {
lock.lock();
try {
//睡眠10s,等待鎖的執行緒此時掛起
System.out.println(Thread.currentThread().getName() + "獲得鎖");
Thread.currentThread().sleep(10000);
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "釋放鎖");
lock.unlock();
}
}
}
複製程式碼
輸出:
Thread-0獲得鎖
Thread-0釋放鎖
Thread-1獲得鎖
Thread-1釋放鎖
複製程式碼
tryLock()是有返回值的,且會立即返回,不會一直等待,如果獲取到鎖,則返回true,否則返回false
public class ThreadLock2 {
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ThreadLock2 mt = new ThreadLock2();
new Thread(() -> mt.insert(Thread.currentThread())).start();
new Thread(() -> mt.insert(Thread.currentThread())).start();
}
public void insert(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println(thread.getName() + "得到了鎖");
} catch (Exception e) {
} finally {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "釋放了鎖");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "獲取鎖失敗");
}
}
}
複製程式碼
輸出:
Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖
複製程式碼
lockInterruptibly允許在等待時由其它執行緒呼叫等待執行緒的Thread.interrupt方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個InterruptedException。
lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為interrupted狀態,然後再中斷執行緒。
示例程式碼:
public class ThreadLock3 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread("1", lock);
MyThread thread2 = new MyThread("2", lock);
thread1.start();
Thread.sleep(100);
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
}
}
class MyThread extends Thread {
private Lock lock = null;
public MyThread(String name, Lock lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
//注意,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面,然後將InterruptedException丟擲
try {
lock.lockInterruptibly();
out.println(this.getName() + "得到了鎖");
long startTime = System.currentTimeMillis();
for (; ; ) {
if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) {
break;
}
}
} catch (InterruptedException e) {
out.println(Thread.currentThread().getName() + "被中斷");
} finally {
out.println(Thread.currentThread().getName() + "執行finally");
}
}
}
複製程式碼
輸出:
1得到了鎖
2被中斷
2執行finally
複製程式碼
ReentranReadWriteLock分為readLock()和writeLock()來獲取讀寫鎖
public class ReadAndWriteLock {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReadAndWriteLock readAndWriteLock = new ReadAndWriteLock();
new Thread(() -> readAndWriteLock.read()).start();
new Thread(() -> readAndWriteLock.read()).start();
new Thread(() -> readAndWriteLock.write()).start();
new Thread(() -> readAndWriteLock.write()).start();
}
public void read() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在進行讀操作");
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) < 10000){}
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "讀操作完畢");
lock.readLock().unlock();
}
}
public void write() {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在進行寫操作");
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) < 10000){}
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "寫操作完畢");
lock.writeLock().unlock();
}
}
}
複製程式碼
輸出:
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢
Thread-2正在進行寫操作
Thread-2寫操作完畢
Thread-3正在進行寫操作
Thread-3寫操作完畢
複製程式碼
Lock和synchronized的選擇:
1.Lock是JDK併發包的一個類,synchronized是關鍵字
2.Lock必須要使用unlock()方法在finally中釋放,如果沒有主動釋放鎖,就有可能導致出現死鎖現象,發生異常時不會自動釋放,synchronized發生異常時會自動釋放
3.Lock在等待獲取鎖的時候可以響應中斷,synchronized則不會。
4.Lock可以知道獲取鎖是否成功
5.Lock有讀寫鎖功能
6.Lock可以在指定的時間範圍內獲取所,如果截止時間到了,依然沒有獲得鎖,則返回複製程式碼