線程安全
14.3 線程安全
14.3.1 線程安全問題
- 需求:A線程將“Hello”存入數組的第一個空位;B線程將“World”存入數組的第一個空位;
- 線程不安全: 當多線程併發訪問臨界資源時,如果破壞原子操作,可能會造成數據不一致;
- 臨界資源:共享資源(同一對象),一次僅允許一個線程使用,纔可保證其正確性;
- 原子操作:不可分割的多步操作,被視作一個整體,其操作和步驟不可打亂或缺省;
14.3.2 同步方式(1)
- 同步代碼塊:
synchronized(臨界資源對象){//對臨界資源對象加鎖
//代碼(原子操作)
} - 注:
每個對象都有一個互斥鎖標記,用來分配給線程的;
只有擁有對象互斥鎖標記的線程,才能進入該對象加鎖的同步代碼塊;
線程退出同步代碼塊時,會釋放相應的互斥鎖標記;
public class TestSynchronized {
public static void main(String[] args) {
//臨界資源對象
//臨界資源對象只有一把鎖
Account acc = new Account("6002" , "1234" , 2000);
//兩個線程對象,共享同一銀行卡資源對象
Thread husband = new Thread(new Husband(acc) , "丈夫");
Thread wife = new Thread(new Wife(acc) , "妻子");
husband.start();
wife.start();
}
}
class Husband implements Runnable{
Account acc;
public Husband(Account acc) {
this.acc = acc;
}
//線程任務:取款
public void run() {
// synchronized(acc) {//對臨界資源對象加鎖
this.acc.withdrawal("6002", "1234", 500);
// }
}
}
class Wife implements Runnable{
Account acc;
public Wife(Account acc) {
this.acc = acc;
}
//線程任務:取款
public void run() {
// synchronized(acc) {
this.acc.withdrawal("6002", "1234", 1200);//原子操作
}
// }
}
//銀行賬戶
class Account{
String cardNo;
String password;
double balance;
public Account(String cardNo , String password , double balance) {
super();
this.cardNo = cardNo;
this.password = password;
this.balance = balance;
}
//取款(原子操作,從插卡驗證,到取款成功的一系列步驟,不可缺少或打斷)
public void withdrawal(String no , String pwd , double money) {
//
synchronized(this) {//對當前共享實例加同步鎖
System.out.println(Thread.currentThread().getName()+"正在讀卡。。。");
if(no.equals(this.cardNo) && pwd.equals(this.password)) {
System.out.println(Thread.currentThread().getName()+"驗證成功。。。");
if(money > this.balance) {
System.out.println(Thread.currentThread().getName()+" 卡內餘額不足");
}else{
try {
Thread.sleep(1000);//模擬ATM數錢時間
}catch(InterruptedException e){
e.printStackTrace();
}
this.balance = this.balance - money;
System.out.println(Thread.currentThread().getName()+"當前餘額爲:"+this.balance);
}
}else {
System.out.println(Thread.currentThread().getName()+"卡號或密碼錯誤");
}
}
}
}
輸出結果:
丈夫正在讀卡。。。
丈夫驗證成功。。。
丈夫當前餘額爲:1500.0
妻子正在讀卡。。。
妻子驗證成功。。。
妻子當前餘額爲:300.0
14.3.3 線程的狀態(阻塞)
- 注:JDK5之後,就緒、運行統稱Runnable
14.3.4 同步方法2
- 同步方法
synchronized 返回值類型 方法名稱(形參列表0){//對當前對象(this)加鎖
//代碼(原子操作)
} - 注:
只有擁有對象互斥鎖標記的線程,才能進入該對象加鎖的同步方法中。
線程退出同步方法時,會釋放相應的互斥鎖標記
14.3.5 同步規則
注意:
- 只有在調用包含同步代碼塊的方法,或者同步方法時,才需要對象的鎖標記;
- 如調用不包含同步代碼塊的方法,或普通方法時,則不需要鎖標記,可直接調用;
已知JDK中線程安全的類:
- StringBuffer
- Vector
- Hashtable
- 以上類中的公開方法,均爲synchronized修飾的同步方法;
14.3.6 經典問題
死鎖:
- 當前第一個線程擁有A對象鎖標記,並等待B對象鎖標記,同時第二個線程擁有B對象鎖標記,並等待A對象鎖標記時,產生死鎖;
- 一個線程可以同時擁有多個對象的鎖標記,當線程阻塞時,不會釋放已經擁有的鎖標記,由此可能造成死鎖;
public class TestDeadLock {
public static void main(String[] args) {
LeftChopstick left = new LeftChopstick();
RightChopstick right = new RightChopstick();
Thread boy = new Thread(new Boy(left , right));
Thread girl = new Thread(new Girl(left , right));
girl.start();
boy.start();
}
}
class LeftChopstick{
String name = "左筷子";
}
class RightChopstick{
String name = "右筷子";
}
class Boy implements Runnable{
LeftChopstick left;
RightChopstick right;
public Boy(LeftChopstick left , RightChopstick right) {
this.left = left;
this.right = right;
}
public void run() {
System.out.println("男孩要拿筷子:");
synchronized(left) {//拿到左筷子資源,加鎖
try {
//高風亮節,把筷子讓出去了
left.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("男孩拿到了左筷子,開始拿右筷子");
synchronized(right) {//拿到右筷子資源,加鎖
System.out.println("男孩拿到了右筷子,開始吃飯");
}
}
}
}
class Girl implements Runnable{
LeftChopstick left;
RightChopstick right;
public Girl(LeftChopstick left , RightChopstick right) {
this.left = left;
this.right = right;
}
public void run() {
System.out.println("女孩要拿筷子:");
synchronized(right) {//拿到右筷子資源,加鎖
System.out.println("女孩拿到了右筷子,開始拿左筷子");
synchronized(left) {//拿到左筷子資源,加鎖
System.out.println("女孩拿到了左筷子,開始吃飯");
left.notify();//女孩吃完後,喚醒等待左筷子鎖的的男孩線程
}
}
}
}
輸出結果:
男孩要拿筷子:
女孩要拿筷子:
女孩拿到了右筷子,開始拿左筷子
女孩拿到了左筷子,開始吃飯
男孩拿到了左筷子,開始拿右筷子
男孩拿到了右筷子,開始吃飯
- 生產者、消費者:
若干個生產者在生產產品,這些產品將提供給若干個消費者去消費,爲了使生產者和消費者能併發執行,在兩者之間設置一個能存儲多個產品的緩衝區,生產者將生產的產品放入緩衝區中,消費者從緩衝區中取走產品進行消費,顯然生產者和消費者之間必須保持同步,即不允許消費者到一個空的緩衝區中取產品,也不允許生產者向一個滿的緩衝區中放入產品;
public class TestProductCustomer {
public static void main(String[] args) {
Shop shop = new Shop();//共享資源對象
Thread p = new Thread(new Product(shop) , "生產者");
Thread c = new Thread(new Customer(shop) , "消費者");
p.start();
c.start();
}
}
//商場
class Shop{
Goods goods;
boolean flag;//標識商品釋放充足
//生產者調用的存的方法
public synchronized void saveGoods(Goods goods) {
//1.判斷商品是否充足
if(flag == true) {//商品充足,生產者不用生產,而等待消費者買完
System.out.println("生產者:商品充足,要等待了");
try {
this.wait();//進入等待狀態
}catch(InterruptedException e) {
e.printStackTrace();
}
}
//商品不足,生產者生產商品,存入商場
System.out.println(Thread.currentThread().getName()+"生產者在商場裏存放"+goods.getId()+"件商品");
this.goods = goods;
flag = true;//已經有商品了,可以讓消費者購買了
this.notifyAll();//將等待隊列的消費者喚醒,可以購買
}
//消費者調用的取的方法
public synchronized void buyGoods() throws InterruptedException{
if(flag == false) {//沒有商品了,消費者需要等待
System.out.println("消費者,商品不充足,要等待了");
this.wait();//消費者進入等待隊列,等待生產者生產商品後,喚醒
}
//正常購買商品
System.out.println(Thread.currentThread().getName()+"購買了"+goods.getId()+"件商品");
//商品賣完了,標識沒貨了
this.goods = null;
flag = false;
//喚醒生產者生產商品
this.notifyAll();
}
}
//商品
class Goods{
private int id;
public Goods(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
//生產者
class Product implements Runnable{
Shop shop;//商場
public Product(Shop shop) {
this.shop = shop;
}
public void run() {
//通過循環生產商品存放到shop裏
for(int i = 1 ; i <= 4 ; i++) {
//生產者線程調用存商品的方法,傳一個商品對象
this.shop.saveGoods(new Goods(i));
}
}
}
//消費者
class Customer implements Runnable{
Shop shop;//商場
public Customer(Shop shop) {
this.shop = shop;
}
public void run() {
for(int i = 1 ; i <= 4 ; i++) {
try {
this.shop.buyGoods();
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
輸出結果:
消費者,商品不充足,要等待了
生產者生產者在商場裏存放1件商品
生產者:商品充足,要等待了
消費者購買了1件商品
生產者生產者在商場裏存放2件商品
生產者:商品充足,要等待了
消費者購買了2件商品
消費者,商品不充足,要等待了
生產者生產者在商場裏存放3件商品
生產者:商品充足,要等待了
消費者購買了3件商品
生產者生產者在商場裏存放4件商品
消費者購買了4件商品
14.3.7 線程通信
等待:
- public final void wait()
- public final void wait(long timeout)
- 必須在對obj加鎖的同步代碼塊中。在一個線程中,調用obj.wait()時,此線程會釋放其擁有的所有鎖標記。同時此線程阻塞在o的等待隊列中。釋放鎖,進入等待隊列。
通知:
- public final void notify()
- public final void notifyAll()
- 必須在對obj加鎖的同步代碼塊中。從obj的Waiting中釋放一個或全部線程。對本身沒有任何影響。
public class TestWaitNotify {
public static void main(String[] args) throws Exception{
Object obj = new Object();
MyThread t1 = new MyThread(obj);
MyThread2 t2 = new MyThread2(obj);
t2.start();
t1.start();
//主線程通知完兩個線程後,休眠
Thread.sleep(2000);
synchronized(obj) {
System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
// obj.wait();//主線程獲得鎖,也主動釋放
//此時此刻等待隊列裏由兩個線程
// obj.notify();//在obj的等待隊列中,隨機喚醒一個線程
obj.notifyAll();//將obj的等待隊列的所有線程都喚醒
System.out.println(Thread.currentThread().getName()+"退出同步代碼塊");
}
}
}
//一個線程持有A對象的鎖,需要B對象的鎖,另一個持有B,想要A
//簡單的,一個線程持有A對象的鎖,另一個線程也想要:阻塞了
class MyThread extends Thread{
Object obj;
public MyThread(Object obj) {
this.obj = obj;
}
public void run() {
synchronized(obj) {
System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
//Thread-0先拿到了鎖,高風亮節,讓給其它線程先拿
try {
obj.wait();//主動釋放當前持有的鎖,並進入無限期等待
}catch(InterruptedException e1){
e1.printStackTrace();
}
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"退出了同步代碼塊");
}
}
}
class MyThread2 extends Thread{
Object obj;
public MyThread2(Object obj) {
this.obj = obj;
}
public void run() {
synchronized(obj) {
try {
obj.wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"退出了同步代碼塊");
// obj.notify();//在obj這個共享對象的等待隊列中,喚醒一個正在等待拿鎖的線程
}
}
}
輸出結果:
Thread-0進入到同步代碼塊
main進入到同步代碼塊
main退出同步代碼塊
Thread-0退出了同步代碼塊
Thread-1進入到同步代碼塊
Thread-1退出了同步代碼塊
14.3.8 總結
線程的創建:
- 方式1:繼承Thread類
- 方式2:實現Runnable接口(一個任務Task),傳入給Thread對象並執行;
線程安全:
- 同步代碼塊:爲方法中的局部代碼(原子操作)加鎖;
- 同步方法:爲方法中的所有代碼(原子操作)加鎖;
線程間的通信:
- wait() / wait(long timeout) :等待
- notify() / notifyAll(): 通知