(Java多執行緒系列二)執行緒間同步
Java多執行緒間同步
1、什麼是執行緒安全
通過一個案例瞭解執行緒安全
案例:需求現在有100張火車票,有兩個視窗同時搶火車票,請使用多執行緒模擬搶票效果。
先來看一個執行緒不安全的例子
class SellTicketRunnable implements Runnable { public int count = 100; @Override public void run() { while (count > 0) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } int index = 100 - count + 1; System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票"); count--; } } } public class JavaSyncDemo { public static void main(String[] args) { SellTicketRunnable runnable = new SellTicketRunnable(); Thread sellThread1 = new Thread(runnable); Thread sellThread2 = new Thread(runnable); sellThread1.start(); sellThread2.start(); } }
可以看到兩個執行緒同時賣票的時候,會出現漏賣,多賣同一張票,還會出現超賣的問題,這就是執行緒不安全的問題。
當多個執行緒同時共享,同一個全域性變數或靜態變數,做寫的操作時,可能會發生資料衝突問題,也就是執行緒安全問題。但是做讀操作是不會發生資料衝突問題。
2、執行緒安全問題的解決辦法
(1)使用同步程式碼塊
class SellTicketRunnable implements Runnable { public int count = 100; private Object lock = new Object(); @Override public void run() { while (count > 0) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock) { if (count > 0) { int index = 100 - count + 1; System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票"); count--; } } } } } public class JavaSyncDemo { public static void main(String[] args) { SellTicketRunnable runnable = new SellTicketRunnable(); Thread sellThread1 = new Thread(runnable); Thread sellThread2 = new Thread(runnable); sellThread1.start(); sellThread2.start(); } }
從上面的案例可以看出,使用synchronized同步程式碼塊包裹住寫操作,每個執行緒在呼叫同步程式碼塊中邏輯的時候,都需要先獲取同步鎖,所以避免了多執行緒寫操作資料的衝突問題。
(2)使用同步函式
class SellTicketRunnable01 implements Runnable { public int count = 100; @Override public void run() { while (count > 0) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } this.sale(); } } synchronized void sale() { if (count > 0) { int index = 100 - count + 1; System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票"); count--; } } } public class JavaSyncDemo01 { public static void main(String[] args) { SellTicketRunnable01 runnable = new SellTicketRunnable01(); Thread sellThread1 = new Thread(runnable); Thread sellThread2 = new Thread(runnable); sellThread1.start(); sellThread2.start(); } }
synchronized包裹的函式,其實就是給該函式塊添加了一把this鎖。
注意:synchronized 修飾靜態方法使用鎖是當前類的位元組碼檔案(即
類名.class
),同理,如果在靜態方法中添加個同步程式碼塊,可以獲取類名.class
為程式碼塊加鎖
class SellTicketRunnable02 implements Runnable {
public static int count = 100;
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
SellTicketRunnable02.sale();
}
}
static void sale() {
synchronized (SellTicketRunnable02.class) {
if (count > 0) {
int index = 100 - count + 1;
System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
count--;
}
}
}
}
public class JavaSyncDemo02 {
public static void main(String[] args) {
SellTicketRunnable02 runnable = new SellTicketRunnable02();
Thread sellThread1 = new Thread(runnable);
Thread sellThread2 = new Thread(runnable);
sellThread1.start();
sellThread2.start();
}
}
(3)使用lock鎖
class SellTicketRunnable03 implements Runnable {
public int count = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
if (count > 0) {
int index = 100 - count + 1;
System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
count--;
}
lock.unlock();
}
}
}
public class JavaSyncDemo03 {
public static void main(String[] args) {
SellTicketRunnable03 runnable = new SellTicketRunnable03();
Thread sellThread1 = new Thread(runnable);
Thread sellThread2 = new Thread(runnable);
sellThread1.start();
sellThread2.start();
}
}
lock和synchronized的區別
①lock在使用時需要手動的獲取鎖和釋放鎖;
②lock可以嘗試非阻塞的獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖;
③lock鎖可以響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常會被丟擲,同時鎖被釋放;
④lock在指定截至時間之前獲取鎖,如果解釋時間到了依舊無法獲取鎖,就返回。
// lock鎖的安全使用方法
class lockDemo {
Lock lock = new ReentrantLock();
void demoFun() {
lock.lock();
try {
// 可能出現執行緒安全的操作
} finally {
lock.unlock();
}
}
}
(4)使用Java原子類
java.util.concurrent.atomic.AtomicBoolean;
java.util.concurrent.atomic.AtomicInteger;
java.util.concurrent.atomic.AtomicLong;
java.util.concurrent.atomic.AtomicReference;
class SellTicketRunnable04 implements Runnable {
public AtomicInteger count = new AtomicInteger(100);
@Override
public void run() {
while (true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count.get() > 0) {
int index = 100 - count.getAndDecrement() + 1;
System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
}
}
}
}
public class JavaSyncDemo04 {
public static void main(String[] args) {
SellTicketRunnable04 runnable = new SellTicketRunnable04();
Thread sellThread1 = new Thread(runnable);
Thread sellThread2 = new Thread(runnable);
sellThread1.start();
sellThread2.start();
}
}
3、死鎖
先看一個死鎖的示例
public class DeadLockDemo01 {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread() { //執行緒1
public void run() {
while (true) {
synchronized (lock1) {
System.out.println(this.getName() + ":獲取lock1鎖");
synchronized (lock2) {
System.out.println(this.getName() + ":獲取lock2鎖");
}
}
}
}
}.start();
new Thread() { //執行緒2
public void run() {
while (true) {
synchronized (lock2) {
System.out.println(this.getName() + ":獲取lock2鎖");
synchronized (lock1) {
System.out.println(this.getName() + "::獲取lock1鎖");
}
}
}
}
}.start();
}
}
執行上面的程式碼,可以觀察到執行緒卡死,就是出現了死鎖
執行緒1先拿到lock1鎖,再拿到lock2鎖,執行完成後才能釋放所有鎖;
執行緒2先拿到lock2鎖,再拿到lock1鎖,執行完成後才能釋放所有鎖。
如果線上程1獲取到lock1鎖的時候,執行緒2獲取到lock2還沒釋放,執行緒1無法獲取lock2鎖,也就無法釋放lock2鎖,這時系統就會出現死鎖。
執行緒死鎖的避免辦法:不要在同步中巢狀同步
原始碼地