1、問題引入
當多個線程共同訪問共享資源的時候,就會發生線程安全的問題,比如如下的程序:
三個窗口同時賣一百張票,但最後出現沒有存在的票(0 -1票)
代碼:
public class TicketRunnableImpl implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0)
//使用sleep 模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 當前線程對應的名字
String name=Thread.currentThread().getName();
System.out.println(name+"正在賣:"+ticket--);
}
}
}
}
public class Demo01Ticket {
public static void main(String[] args) {
TicketRunnableImpl tck=new TicketRunnableImpl();
Thread thread1= new Thread(tck,"窗口1");
Thread thread2 = new Thread(tck,"窗口2");
Thread thread3 = new Thread(tck,"窗口3");
// 同時賣票
thread1.start();
thread2.start();
thread3.start();
}
}
以上,最後窗口賣出了不存在的票,就是說最後窗口3已經把最後一個票賣出去了,但是窗口 1, 2還都不知道。還有可能同一個票被賣了兩回,這種問題就是線程不安全,當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行,導致共享數據的錯誤。
解決線程不安全的辦法就是線程同步。
2、線程同步
由於多個線程執行的不確定性會引起執行結果的不穩定,所以對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。
2.1 同步代碼塊實現線程同步
格式:
synchronized(同步鎖){
需要同步操作的代碼
}
同步鎖:
對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.
-
鎖對象 可以是任意類型。
-
多個線程對象 要使用同一把鎖。
注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着
使用同步代碼塊解決代碼:
public class TicketRunnableImpl implements Runnable {
private int ticket = 100;
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
//使用sleep 模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 當前線程對應的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
}
}
結果,沒有在出現0 -1票
2.2 同步代碼塊實現線程同步
public synchronized void method(){
可能會產生線程安全問題的代碼
}
public class TicketRunnableImpl implements Runnable{
private int ticket = 100;
Object lock = new Object();
@Override
public void run() {
while (true) {
sellTicket();
}
}
* 鎖對象 是 誰調用這個方法 就是誰
* 隱含 鎖對象 就是 this
public synchronized void sellTicket(){
if (ticket > 0) {
//使用sleep 模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 當前線程對應的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
那麼此時的同步鎖是誰:
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。
2.3 Lock 鎖實現線程同步
java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和內存語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖luck()、釋放鎖unlock()。我們在編碼的過程中一定要記得最後要釋放鎖,所以要避免忘記釋放鎖(發生死鎖),要提前寫好下面的模板
public void m(){
lock.lock();
try{
//保證線程安全的代碼;
}
finally{
lock.unlock();
}
}
對應代碼:
private int ticket =100;
Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
try {
if (ticket > 0) {
//使用sleep 模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 當前線程對應的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}finally {
lock.unlock();
}
}
}
3、synchronized與Lock 的對比
1、二者都可以解決線程安全問題
2、Lock是顯示鎖(手動開啓和關閉鎖),synchronized是隱式鎖,在執行完相應的代碼以後,自動釋放鎖。
3、Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
4、釋放鎖的操作
當前線程的同步方法、同步代碼塊執行結束。
當前線程在同步代碼塊、同步方法中遇到break、return終止了該代碼塊、
該方法的繼續執行。
當前線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導
致異常結束。
當前線程在同步代碼塊、同步方法中執行了線程對象的**wait()**方法,當前線
程暫停,並釋放鎖。
注意:yield() 和 sleep並不會釋放鎖