文章目錄
1、線程安全
1.1 一個簡單的線程安全例子
1)下面的例子是,有一張銀行卡,裏面有1000的餘額,兩個人同時在取款機上取錢。
package thread;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456", 1000);
DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
Thread myThread1 = new Thread(drawMoneyRunnable);
Thread myThread2 = new Thread(drawMoneyRunnable);
myThread1.start();
myThread2.start();
}
}
class DrawMoneyRunnable implements Runnable {
private Account account;
private double drawAmount;
public DrawMoneyRunnable(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
}
public void run() {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取錢成功, 取出錢數爲:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("餘額爲:" + balance + "\n");
}else {
System.out.println("取錢失敗!");
System.out.println("餘額爲:" + account.getBalance()+",不夠你要取出的數目。");
}
}
}
class Account {
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
2)程序的輸出結果:
也就是說,對於一張只有1000餘額的銀行卡,兩個人一共可以取出1400,這顯然是有問題的。
1.2 什麼是線程安全
-
線程安全,其實是指多線程環境下對共享資源的訪問可能會引起此共享資源的不一致性。因此,爲避免線程安全問題,應該避免多線程環境下對此共享資源的併發訪問。
-
“非線程安全”問題存在於“實例變量”中,如果是方法內部的私有變量,則不存在“非線程安全”問題,所得結果也就是“線程安全”的了。
-
如果兩個線程同時操作對象中的實例變量,則會出現“非線程安全”,解決辦法就是在方法前加上synchronized關鍵字即可。
2、synchronized——同步鎖
- 如果不知道什麼是鎖,這裏有:多線程基礎篇
2.1 同步方法
1)什麼是同步方法:
- 對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱爲同步方法。可以簡單理解成對此方法進行了加鎖,其鎖對象爲當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要獲得此同步鎖(且同時最多隻有一個線程能夠獲得),只有當線程執行完此同步方法後,纔會釋放鎖對象,其他的線程纔有可能獲取此同步鎖,訪問此方法,以此類推…
2)將上面代碼中的run()方法改爲同步方法:
public synchronized void run() {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取錢成功, 取出錢數爲:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("餘額爲:" + balance + "\n");
}else {
System.out.println("取錢失敗!");
System.out.println("餘額爲:" + account.getBalance()+",不夠你要取出的數目。");
}
}
- 輸出結果如下:
3)synchronized同步方法與鎖對象(重點)
-
synchronized本身沒有鎖的功能,但是他能獲取對象鎖,何爲對象鎖?Java的每個對象都有一個內置鎖,簡稱對象鎖。synchronized同步方法就是給這個方法加了一個對象鎖,其鎖對象爲當前方法所在的對象自身,也就是哪個對象調用這個方法,就給這個方法加那個對象的鎖。
-
思考這個問題:在一個類中,有兩個同步方法a() 和 b()(方法前面有synchronized修飾),這兩個方法沒有任何關係。如果說現在有一個線程A正在訪問其中一個方法a(),在訪問的這段時間,又來了一個線程B,那麼線程B能不能訪問方法b()呢?
-
肯定是不行的。爲什麼呢,因爲同步方法的鎖是調用這個方法的對象的內置鎖,兩個方法在同一個類,肯定是同一個對象調用這兩個方法,也就是同一個對象鎖,既然是同一個對象鎖,肯定是不可以的。
-
上面說的這個性質,其實也就是鎖的互斥性。看下面代碼:如果線程A先執行,則會進去死循環,線程B一直處於同步阻塞的狀態,因爲線程A一直沒有釋放鎖資源;如果線程B先執行,則會輸出這個"B進入test2方法",然後釋放鎖資源,這時候線程A纔可以執行。
class MyThreadX implements Runnable{ @Override public void run() { test1(); test2(); } private synchronized void test2() { if(Thread.currentThread().getName().equals("B")){ System.out.println("B進入test2方法"); } } private synchronized void test1() { if(Thread.currentThread().getName().equals("A")){ while (true){} } } } public class Test12 { public static void main(String[] args) { MyThreadX mt=new MyThreadX(); Thread thread=new Thread(mt,"A"); Thread thread1=new Thread(mt,"B"); thread.start(); thread1.start(); } }
4)synchronized鎖的可重入性(重點)
-
可重入鎖就是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。
class MyThreadX implements Runnable { @Override public void run() { test1(); } private synchronized void test2() { System.out.println(Thread.currentThread().getName() + "進入test2方法"); } private synchronized void test1() { if (Thread.currentThread().getName().equals("A")){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("A進入test1方法"); test2(); } } } public class Test12 { public static void main(String[] args) { MyThreadX mt = new MyThreadX(); Thread thread = new Thread(mt, "A"); thread.start(); } }
-
可重入鎖也支持在父子類繼承的環境中,說明當存在父子類繼承關係時,子類是完全可以通過“可重入鎖”調用父類的同步方法。
5)同步方法不具有繼承性(重點)
- 如果父類有一個帶synchronized關鍵字的方法,子類繼承並重寫了這個方法。
但是同步不能繼承,所以還是需要在子類方法中添加synchronized關鍵字。
6)synchronized同步方法的缺點
- 使用synchronized關鍵字聲明方法有些時候是有很大的弊端的,比如我們有兩個線程一個線程A調用同步方法後獲得鎖,那麼另一個線程B就需要等待A執行完,但是如果說A執行的是一個很費時間的任務的話這樣就會很耗時。
- 也就是說,同步方法鎖定的範圍太大了,這個時候,我們就可以考慮使用 同步代碼塊。
2.2 同步代碼塊
1)什麼是同步代碼塊:
-
正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不確定性即可。使用同步方法時,使得整個方法體都成爲了同步執行狀態,會使得可能出現同步範圍過大的情況,於是,針對需要同步的代碼可以直接另一種同步方式——同步代碼塊來解決。
-
同步代碼塊就是個一段代碼,加一個對象鎖,和同步方法效果一樣,只不過是範圍更加小了。
synchronized (obj) { //... }
2)將上面的安全例子改成同步代碼塊的形式:
public void run() {
synchronized (account) {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取錢成功, 取出錢數爲:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("餘額爲:" + balance + "\n");
} else {
System.out.println("取錢失敗!");
System.out.println("餘額爲:" + account.getBalance() + ",不夠你要取出的數目。");
}
}
}
- 輸出結果:
3)同步代碼塊的鎖對象(重點):
- 同步代碼後面跟着一個對象obj,
synchronized (obj)
,obj是哪個對象,就代表着,這個代碼塊使用的是那個對象的鎖,選擇哪一個對象作爲鎖是至關重要的。 - 如果使用
this
,就代表鎖對象是執行這個代碼塊的對象的鎖,其實也就是和同步方法的鎖對象是一個道理。 - 一般情況下:都是選擇此共享資源對象作爲鎖對象。如上例中,最好選用account對象作爲鎖對象。(當然,選用this也是可以的,那是因爲創建線程使用了runnable方式,如果是直接繼承Thread方式創建的線程,使用this對象作爲同步鎖會其實沒有起到任何作用,因爲是不同的對象了。因此,選擇同步鎖時需要格外小心…)
4)同步代碼塊間的同步性(重點):
-
當一個對象訪問synchronized(this)代碼塊時,其他線程對同一個對象中所有其他synchronized(this)代碼塊代碼塊的訪問將被阻塞,這說明synchronized(this)代碼塊使用的“對象監視器”是一個。
也就是說和synchronized方法一樣,synchronized(this)代碼塊也是鎖定當前對象的。 -
另外通過上面的學習我們可以得出兩個結論。
- 其他線程執行對象中synchronized同步方法(上一節我們介紹過,需要回顧的可以看上一節的文章)和synchronized(this)代碼塊時呈現一樣的同步效果,只不過後者的作用範圍跟小一些;
- 如果兩個線程使用了同一個“對象監視器”,運行結果同步,否則不同步.
2.3 靜態同步方法/代碼塊
-
synchronized關鍵字加到static靜態方法和synchronized(class)代碼塊上都是是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。
// 共享資源 class Service { //靜態方法,獲取的Class鎖 public static void printA() { synchronized (Service.class) { //這裏要用類的class對象 try { System.out.println( "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA"); Thread.sleep(3000); System.out.println( "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA"); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public static void printB() { System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB"); System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB"); } synchronized public void printC() { System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printC"); System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printC"); } } class ThreadA extends Thread { private Service service; public ThreadA(Service service) { super(); this.service = service; } @Override public void run() { Service.printA(); } } class ThreadB extends Thread { private Service service; public ThreadB(Service service) { super(); this.service = service; } @Override public void run() { Service.printB(); } } class ThreadC extends Thread { private Service service; public ThreadC(Service service) { super(); this.service = service; } @Override public void run() { service.printC(); } } public class Run { public static void main(String[] args) { Service service = new Service(); ThreadA a = new ThreadA(service); a.setName("A"); a.start(); ThreadB b = new ThreadB(service); b.setName("B"); b.start(); ThreadC c = new ThreadC(service); c.setName("C"); c.start(); } }
運行結果:
-
從運行結果可以看出:線程A,B和線程C持有的鎖不一樣,所以A和B運行同步,但是和C運行不同步。
-
靜態同步synchronized方法與synchronized(class)代碼塊持有的鎖一樣,都是Class鎖,Class類鎖對所有的使用
Class.class
對象鎖的方法或代碼塊起作用。synchronized關鍵字加到非static靜態方法上持有的是對象鎖,與Class類鎖不會產生同步。
2.4 synchronized釋放鎖的時機
-
當方法(代碼塊)執行完畢後會自動釋放鎖,不需要做任何的操作。
-
當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。不會由於異常導致出現死鎖現象~
-
如果只是進入阻塞狀態,不會釋放鎖,比如調用sleep方法。
3、Lock——顯式鎖
3.1 Lock鎖概述
Lock鎖,可以得到和 synchronized一樣的效果,即實現原子性、有序性和可見性。
相較於synchronized,Lock鎖可手動獲取鎖和釋放鎖、可中斷的獲取鎖、超時獲取鎖。
Lock 是一個接口,兩個直接實現類:
- ReentrantLock(重入鎖)
- ReentrantReadWriteLock(讀寫鎖)。
1)有什麼辦法方便同步鎖對象與共享資源解耦,同時又能很好的解決線程安全問題?
-
使用Lock對象同步鎖可以方便的解決此問題,唯一需要注意的一點是Lock對象需要與資源對象同樣具有一對一的關係。Lock對象同步鎖一般格式爲:
class X { // 顯示定義Lock同步鎖對象,此對象與共享資源具有一對一關係 private final Lock lock = new ReentrantLock(); public void m() { // 加鎖 lock.lock(); try { // ... 需要進行線程安全同步的代碼 } finally { // 釋放Lock鎖 lock.unlock(); } } }
2)給上面的線程安全問題加上Lock鎖:
-
l.lock()方法進行上鎖, l.unlock()方法進行解鎖
class DrawMoneyRunnable implements Runnable { private Account account; private double drawAmount; private final Lock lock = new ReentrantLock(); public DrawMoneyRunnable(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } public void run() { lock.lock(); try { if (account.getBalance() >= drawAmount) { // 1 System.out.println("取錢成功, 取出錢數爲:" + drawAmount); double balance = account.getBalance() - drawAmount; account.setBalance(balance); System.out.println("餘額爲:" + balance + "\n"); } else { System.out.println("取錢失敗!"); System.out.println("餘額爲:" + account.getBalance() + ",不夠你要取出的數目。"); } } finally { lock.unlock(); } } }
3.2 Lock鎖 與 synchronized鎖比較
1)兩者的區別:
-
首先synchronized是java內置關鍵字,在jvm層面,Lock是個java接口,他有實現類;
-
synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
-
synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;
-
用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;
-
synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)
-
Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。
4、wait()/notify()/notifyAll()線程通信
雖然這三個方法主要都是用於多線程中,但實際上都是Object類中的本地方法。因此,理論上,任何Object對象都可以作爲這三個方法的主調,在實際的多線程編程中,只有同步鎖對象調這三個方法,才能完成對多線程間的線程通信。
1)三個方法的api
-
wait():導致當前線程等待並使其進入到等待阻塞狀態。直到其他線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。
-
notify():喚醒在此同步鎖對象上等待的單個線程,如果有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操作,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。
-
notifyAll():喚醒在此同步鎖對象上等待的所有線程,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。
package com.qqyumidi;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456", 0);
Thread drawMoneyThread = new DrawMoneyThread("取錢線程", account, 700);
Thread depositeMoneyThread = new DepositeMoneyThread("存錢線程", account, 700);
drawMoneyThread.start();
depositeMoneyThread.start();
}
}
class DrawMoneyThread extends Thread {
private Account account;
private double amount;
public DrawMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++) {
account.draw(amount, i);
}
}
}
class DepositeMoneyThread extends Thread {
private Account account;
private double amount;
public DepositeMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++) {
account.deposite(amount, i);
}
}
}
class Account {
private String accountNo;
private double balance;
// 標識賬戶中是否已有存款
private boolean flag = false;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 存錢
*
* @param depositeAmount
*/
public synchronized void deposite(double depositeAmount, int i) {
if (flag) {
// 賬戶中已有人存錢進去,此時當前線程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " -- i=" + i);
wait();
// 1
System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 開始存錢
System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
setBalance(balance + depositeAmount);
flag = true;
// 喚醒其他線程
notifyAll();
// 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-- 存錢 -- 執行完畢" + " -- i=" + i);
}
}
/**
* 取錢
*
* @param drawAmount
*/
public synchronized void draw(double drawAmount, int i) {
if (!flag) {
// 賬戶中還沒人存錢進去,此時當前線程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 開始要執行wait操作" + " 執行了wait操作" + " -- i=" + i);
wait();
System.out.println(Thread.currentThread().getName() + " 執行了wait操作" + " 執行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 開始取錢
System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount + " -- i=" + i);
setBalance(getBalance() - drawAmount);
flag = false;
// 喚醒其他線程
notifyAll();
System.out.println(Thread.currentThread().getName() + "-- 取錢 -- 執行完畢" + " -- i=" + i); // 3
}
}
}
2)要注意的點:
-
1.wait()方法執行後,當前線程立即進入到等待阻塞狀態,其後面的代碼不會執行;
-
2.notify()/notifyAll()方法執行後,將喚醒此同步鎖對象上的(任意一個-notify()/所有-notifyAll())線程對象,但是,此時還並沒有釋放同步鎖對象,也就是說,如果notify()/notifyAll()後面還有代碼,還會繼續進行,知道當前線程執行完畢纔會釋放同步鎖對象;
-
3.notify()/notifyAll()執行後,如果右面有sleep()方法,則會使當前線程進入到阻塞狀態,但是同步對象鎖沒有釋放,依然自己保留,那麼一定時候後還是會繼續執行此線程,接下來同2;
-
4.wait()/notify()/nitifyAll()完成線程間的通信或協作都是基於不同對象鎖的,因此,如果是不同的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關係;
-
5.當wait線程喚醒後並執行時,是接着上次執行到的wait()方法代碼後面繼續往下執行的。
5、volatile關鍵字
- 修飾變量,保證變量的可見性,不保證原子性。
5.1 volatile關鍵字的可見性
1) 補個下面用到的知識點:
-
主存是公共空間,基本可以類比爲虛擬機模型中的堆,對象創建好了都是在主存裏,所有線程都可以訪問(共享)。
-
工作內存(下文所說的本地內存)是線程的私有內存,只有本線程可以訪問,如果線程要操作主存中的某個對象,必須從主存中拷貝到工作內存,在對工作內存中的副本進行操作,操作後再寫入主存,而不能對主存的對象直接操作 。
2) volatile 修飾的成員變量在每次被線程訪問時,都強迫從主存(共享內存)中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到主存(共享內存)。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值,這樣也就保證了同步數據的可見性。
package thread.syn;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已經賦值爲false");
}
}
class RunThread extends Thread {
private boolean isRunning = true;
int m;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("進入run了");
while (isRunning == true) {
int a = 2;
int b = 3;
int c = a + b;
m = c;
}
System.out.println(m);
System.out.println("線程被停止了!");
}
}
運行結果:死循環
-
RunThread類中的isRunning變量沒有加上volatile關鍵字時,運行以上代碼會出現死循環,這是因爲isRunning變量雖然被修改但是沒有被寫到主存中,這也就導致該線程在本地內存中的值一直爲true,這樣就導致了死循環的產生。
-
解決辦法也很簡單:isRunning變量前加上volatile關鍵字即可。
3)注意下面這個問題:
假如你把while循環代碼里加上任意一個輸出語句或者sleep方法你會發現死循環也會停止,不管isRunning變量是否被加上了上volatile關鍵字。
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
System.out.println(m);
}
//或者:
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
- 這是爲什麼?
JVM會盡力保證內存的可見性,即便這個變量沒有加同步關鍵字。換句話說,只要CPU有時間,JVM會盡力去保證變量值的更新。這種與volatile關鍵字的不同在於,volatile關鍵字會強制的保證線程的可見性。而不加這個關鍵字,JVM也會盡力去保證可見性,但是如果CPU一直有其他的事情在處理,它也沒辦法。最開始的代碼,一直處於死循環中,CPU處於一直佔用的狀態,這個時候CPU沒有時間,JVM也不能強制要求CPU分點時間去取最新的變量值。而加了輸出或者sleep語句之後,CPU就有可能有時間去保證內存的可見性,於是while循環可以被終止。
5.2 volatile關鍵字能保證原子性嗎?
volatile是無法保證原子性的,要保證數據的原子性還是要使用synchronized關鍵字。
5.3 synchronized關鍵字和volatile關鍵字比較
- volatile關鍵字是線程同步的輕量級實現,所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用於變量而synchronized關鍵字可以修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用synchronized關鍵字還是更多一些。
- 多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
- volatile關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized關鍵字兩者都能保證。
- volatile關鍵字用於解決變量在多個線程之間的可見性,而ynchronized關鍵字解決的是多個線程之間訪問資源的同步性。
文章參考地址,這部分內容基本上都是出自這裏:https://blog.csdn.net/qq_34337272/article/details/79680771