JavaEE學習日誌持續更新----> 必看!JavaEE學習路線(文章總彙)
線程安全
產生原理
若有三個電影院同時上映了戰狼3,每個電影院要賣100張不同號碼的電影票,三位電影院老闆有了三種不同的賣票思路。
第一位老闆:設置了一個售賣窗口,這個窗口賣1-100號碼的票,則不會出現問題,此時則爲單線程程序,不會出現線程安全問題。
第二位老闆:設置了三個售賣窗口,第一個窗口賣1-33號的票,第二個窗口賣34-67號的票,第三個窗口賣68-100號的票,則不會出現問題,此時則爲多線程程序,但不訪問共享資源,不會出現線程安全問題。
第三位老闆:設置了三個售賣窗口,三個窗口同時賣1-100號的票,則出現了問題,此時則爲多線程程序,訪問了共享資源,會出現線程安全問題。
代碼模擬:
/*
賣票案例
*/
public class RunnableImpl implements Runnable {
//定義共享票源
private int ticket = 100;
//線程任務:賣票
@Override
public void run() {
//賣票重複執行
while(true){
//增加一個判斷,票大於0
if(ticket>0){
//添加sleep方法,增加線程安全問題出現的機率
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作
System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票!");
ticket--;
} else {
break;
}
}
}
}
/*
開啓三個線程,同時進行賣票
*/
public class Demo01PayTicket {
public static void main(String[] args) {
//創建Runnable接口的實現類對象
RunnableImpl r = new RunnableImpl();
//創建三個線程
Thread t0 = new Thread(r,"售票員A");
Thread t1 = new Thread(r,"售票員B");
Thread t2 = new Thread(r,"售票員C");
//開啓新的線程
t0.start();
t1.start();
t2.start();
}
}
結果:出現安全問題
...
售票員C正在賣第16張票!
售票員A正在賣第16張票!
售票員B正在賣第14張票!
售票員A正在賣第13張票!
售票員C正在賣第12張票!
售票員B正在賣第11張票!
售票員A正在賣第10張票!
售票員C正在賣第10張票!
售票員B正在賣第8張票!
售票員A正在賣第7張票!
售票員C正在賣第7張票!
售票員B正在賣第5張 票!
售票員A正在賣第4張票!
售票員C正在賣第4張票!
售票員B正在賣第2張票!
售票員C正在賣第1張票!
售票員A正在賣第0張票!
售票員B正在賣第-1張票!
線程安全問題產生的原因:
售票員C正在賣第1張票!
售票員A正在賣第0張票!
售票員B正在賣第-1張票!
第一種安全問題:出現-1和0張票的原因(出現超出if語句範圍的數值)
①售票員C線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
②售票員A線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
③售票員B線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
④售票員C線程睡醒了,繼續執行程序進行賣票。
售票員C正在賣第1張票!
ticket--;ticket = 0;
`⑤售票員A線程睡醒了,繼續執行程序進行賣票。
售票員A正在賣第0張票
ticket--;ticket = -1;
`⑥售票員B線程睡醒了,繼續執行程序進行賣票。
售票員B正在賣第-1張票!
ticket--;ticket = -2;
第二種安全問題:出現重複賣票的原因
售票員A正在賣第10張票!
售票員C正在賣第10張票!
售票員B正在賣第8張票!
售票員A和售貨員C兩個線程都在打印正在賣第10張票,這時候ticket還沒有進行自減操作。之後ticket自減兩次,所以第9張票消失,售票員B只能賣第8張票。
線程同步
解決線程安全的第一種方式:同步代碼塊
格式:
synchronized(鎖對象){
出現安全問題的代碼(訪問了共享數據的代碼)
}
注意事項:
- 鎖對象可以是任意對象 new Person,new Student
- 鎖對象必須保證多個線程使用的是同一個鎖對象
- 鎖對象的作用:把括號中的代碼鎖住,只讓一個線程進去執行
示例:賣票案例
public class RunnableImpl implements Runnable {
private int ticket = 100;
//在成員位置創建一個鎖對象
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
//訪問了共享數據的代碼
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票!");
ticket--;
}
}
}
}
}
public class Demo01PayTicket {
public static void main(String[] args) {
//創建Runnable接口的實現類對象
RunnableImpl r = new RunnableImpl();
//創建三個線程
Thread t0 = new Thread(r,"售票員A");
Thread t1 = new Thread(r,"售票員B");
Thread t2 = new Thread(r,"售票員C");
//開啓新的線程
t0.start();
t1.start();
t2.start();
}
}
同步技術的原理:使用了一個鎖對象,這個鎖對象也叫同步鎖,還叫對象監視器
當線程執行到同步代碼塊的時候,會判斷同步代碼中,是否有鎖對象:
如果有,那麼這個線程就會獲取鎖對象,進入到同步中執行,在執行過程中,無論是否失去了CPU的執行權,都不會釋放鎖對象,線程執行完同步中的代碼,纔會把鎖對象歸還給同步代碼。
如果沒有,那麼這個線程就會進入阻塞狀態,在同步外邊一直等待同步中的線程歸還鎖對象,直到同步中的線程把鎖對象歸還,才能獲取鎖對象進入到同步中執行。
總結:
- 沒有鎖對象,進不去同步
- 同步中的對象沒有執行完,也不會歸還鎖
這樣就保證只有一個線程在同步中執行,代碼就安全了
問題:程序頻繁判斷鎖,獲取鎖,歸還鎖,效率就會降低
解決線程安全的第二種方式:同步方法
格式:
修飾符 synchronized 返回值類型 方法名(參數列表){
出現安全問題的代碼
}
使用步驟:
- 創建一個方法,方法的修飾符添加上synchronized
- 把訪問了共享數據的代碼放入到方法中
- 調用同步方法
注意:同步方法的鎖對象就是this(本類對象 new RunnableImpl)
示例:賣票案例
public class RunnableImpl implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
//3.調用同步方法
payTicket();
}
}
/*
定義一個方法
1.創建一個方法,方法的修飾符添加上synchronized
*/
public synchronized void payTicket() {
//2.把訪問了共享數據的代碼放入到方法中
//訪問了共享數據的代碼
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
ticket--;
}
}
}
public class Demo01PayTicket {
public static void main(String[] args) {
//創建Runnable接口的實現類對象
RunnableImpl r = new RunnableImpl();
//創建三個線程
Thread t0 = new Thread(r,"售票員A");
Thread t1 = new Thread(r,"售票員B");
Thread t2 = new Thread(r,"售票員C");
//開啓新的線程
t0.start();
t1.start();
t2.start();
}
}
擴展:靜態同步方法
靜態同步方法(優先於對象加載到內存中):鎖對象是的class文件對象(反射)RunnableImpl.class–>唯一
示例:賣票案例
public class RunnableImpl implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
//3.調用同步方法
payTicket();
}
}
/*
定義一個方法
1.創建一個方法,方法的修飾符添加上synchronized
*/
//靜態同步方法(優先於對象加載到內存中):鎖對象是的class文件對象(反射)RunnableImpl.class-->唯一
public static synchronized void payTicket() {
//2.把訪問了共享數據的代碼放入到方法中
//訪問了共享數據的代碼
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
ticket--;
}
}
}
public class Demo01PayTicket {
public static void main(String[] args) {
//創建Runnable接口的實現類對象
RunnableImpl r = new RunnableImpl();
//創建三個線程
Thread t0 = new Thread(r,"售票員A");
Thread t1 = new Thread(r,"售票員B");
Thread t2 = new Thread(r,"售票員C");
//開啓新的線程
t0.start();
t1.start();
t2.start();
}
}
解決線程安全的第三種方式:使用Lock鎖
java.util.concurrent.locks.Lock接口
JDK1.5之後的新特性,Lock實現提供了比使用synchronized方法和語句可以獲得的更廣泛的鎖定操作。
Lock接口中的方法:
void lock()
獲得鎖。void unlock()
釋放鎖。
實現類:java.util.concurrent.locks.ReentrantLock implements Lock
使用步驟:
- 在成員位置創建一個Lock接口的實現類對象ReentrantLock
- 在可能會出現安全問題的代碼前,調用Lock方法獲取鎖對象
- 在可能會出現安全問題的代碼後,調用Lock方法釋放鎖對象
示例:賣票案例
public class RunnableImpl implements Runnable {
//1.在成員位置創建一個Lock接口的實現類對象ReentrantLock
Lock l = new ReentrantLock();
private int ticket = 100;
@Override
public void run() {
while (true) {
//2.在可能會出現安全問題的代碼前,調用Lock方法獲取鎖對象
l.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//無論程序是否異常,都會把鎖對象釋放,節約內存,提高程序效率
//3.在可能會出現安全問題的代碼後,調用Lock方法釋放鎖對象
l.unlock();
}
}
}
}
}
public class Demo01PayTicket {
public static void main(String[] args) {
//創建Runnable接口的實現類對象
RunnableImpl r = new RunnableImpl();
//創建三個線程
Thread t0 = new Thread(r,"售票員A");
Thread t1 = new Thread(r,"售票員B");
Thread t2 = new Thread(r,"售票員C");
//開啓新的線程
t0.start();
t1.start();
t2.start();
}
}