本文目錄:
1.幾個基本的概念
2.建立線程的兩種方法
3.線程相關的經常使用方法
4.多線程安全問題和線程同步
4.1 多線程安全問題
4.2 線程同步
4.3 同步代碼塊和同步函數的區別以及鎖是什麼
4.4 單例懶漢模式的多線程安全問題
5.死鎖(DeadLock) javascript
本文涉及到的一些概念,有些是基礎知識,有些在後文會展開詳細的說明。php
還需須要明確的一個關鍵點是:CPU對就緒隊列中每一個線程的調度是隨機的(對咱們人類來講),且分配的時間片也是隨機的(對人類來講)。css
Java中有兩種建立線程的方式。html
建立線程方式一: java
例以下面的代碼中,在主線程main中建立了兩個線程對象,前後並前後調用start()開啓這兩個線程,這兩個線程會各自執行MyThread中的run()方法。nginx
class MyThread extends Thread {
String name;
String gender;
MyThread(String name,String gender){
this.name = name;
this.gender = gender;
}
public void run(){
int i = 0;
while(i<=20) {
//除了主線程main,其他線程從0開始編號,currentThread()獲取的是當前線程對象
System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender);
i++;
}
}
}
public class CreateThread {
public static void main(String[] args) {
MyThread mt1 = new MyThread("malong","Male");
MyThread mt2 = new MyThread("Gaoxiao","Female");
mt1.start();
mt2.start();
System.out.println("main thread over");
}
}
上面的代碼執行時,有三個線程,首先是主線程main建立2個線程對象,並開啓這兩個線程任務,開啓兩個線程後主線程輸出"main thread over",而後main線程結束。在開啓兩個線程任務後,這兩個線程加入到了就緒隊列等待CPU的調度執行。以下圖。由於每一個線程被cpu調度是隨機的,執行時間也是隨機的,因此即便mt1先開啓任務,但mt2可能會比mt1線程先執行,也可能更先消亡。git
建立線程方式二:github
class MyThread implements Runnable {
String name;
String gender;
MyThread(String name,String gender){
this.name = name;
this.gender = gender;
}
public void run(){
int i = 0;
while(i<=200) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
i++;
}
}
}
public class CreateThread2 {
public static void main(String[] args) {
//建立子類對象
MyThread mt = new MyThread("malong","Male");
//建立線程對象
Thread th1 = new Thread(mt);
Thread th2 = new Thread(mt);
th1.start();
th2.start();
System.out.println("main thread over");
}
}
這兩種建立線程的方法,無疑第二種(實現Runnable接口)要好一些,由於第一種建立方法繼承了Thread後就沒法繼承其餘父類。web
Thread類中的方法:django
isAlive()
:判斷線程是否還活着。活着的概念是指是否消亡了,對於運行態、就緒態、睡眠態的線程都是活着的狀態。currentThread()
:返回值爲Thread,返回當前線程對象。getName()
:獲取當前線程的線程名稱。setName()
:設置線程名稱。給線程命名還可使用構造方法Thread(String thread_name)
或Thread(Runnable r,String thread_name)
。getPriority()
:獲取線程優先級。優先級範圍值爲1-10(默認值爲5),相鄰值之間的差距對cpu調度的影響很小。通常使用3個字段MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分別表示一、五、10三個優先級,這三個優先級可較大地區分cpu的調度。setPriority()
:設置線程優先級。run()
:封裝的是線程開啓後要執行的任務代碼。若是run()中沒有任何代碼,則線程不作任何事情。start()
:開啓線程並讓線程開始執行run()中的任務。toString()
:返回線程的名稱、優先級和線程組。sleep(long millis)
:讓線程睡眠多少毫秒。join(t1)
:將線程t1合併到當前線程,並等待線程t1執行完畢後才繼續執行當前線程。即讓t1線程強制插隊到當前線程的前面並等待t1完成。yield()
:將當前正在執行的線程退讓出去,以讓就緒隊列中的其餘線程有更大的概率被cpu調度。即強制本身放棄cpu,並將本身放入就緒隊列。因爲本身也在就緒隊列中,因此即便此刻本身放棄了cpu,下一次仍是可能會當即被cpu選中調度。但畢竟給了機會給其它就緒態線程,因此其餘就緒態線程被選中的概率要更大一些。Object類中的方法:
wait()
:線程進入某個線程池中並進入睡眠態。等待notify()或notifyAll()的喚醒。notify()
:從某個線程池中隨機喚醒一個睡眠態的線程。notifyAll()
:喚醒某個線程池中全部的睡眠態線程。這裏的某個線程池是由鎖對象決定的。持有相同鎖對象的線程屬於同一個線程池。見後文。
通常來講,wait()和喚醒的notify()或notifyAll()是成對出現的,不然很容易出現死鎖。
sleep()和wait()的區別:(1)所屬類不一樣:sleep()在Thread類中,wait()則是在Object中;(2)sleep()能夠指定睡眠時間,wait()雖然也能夠指定睡眠時間,但大多數時候都不會去指定;(3)sleep()不會拋異常,而wait()會拋異常;(4)sleep()能夠在任何地方使用,而wait()必須在同步代碼塊或同步函數中使用;(5)最大的區別是sleep()睡眠時不會釋放鎖,不會進入特定的線程池,在睡眠時間結束後自動甦醒並繼續往下執行任務,而wait()睡眠時會釋放鎖,進入線程池,等待notify()或notifyAll()的喚醒。
java.util.concurrent.locks包中的類和它們的方法:
Lock類中:
lock()
:獲取鎖(互斥鎖)。unlock()
:釋放鎖。newCondition()
:建立關聯此lock對象的Condition對象。Condition類中:
await()
:和wait()同樣。signal()
:和notify()同樣。signalAll()
:和notifyAll()同樣。線程安全問題是指多線程同時執行時,對同一資源的併發操做會致使資源數據的混亂。
例以下面是用多個線程(窗口)售票的代碼。
class Ticket implements Runnable {
private int num; //票的數量
Ticket(int num){
this.num = num;
}
//售票
public void sale() {
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
//獲取剩餘票數
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
public class ConcurrentDemo {
public static void main(String[] args) {
Ticket t = new Ticket(100);
//建立多個線程對象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//開啓多個線程使其執行任務
t1.start();
t2.start();
t3.start();
t4.start();
}
}
執行結果大體以下:
以上代碼的執行過程大體以下圖:
共開啓了4個線程執行任務(不考慮main主線程),每個線程都有4個任務:
if(num>0)
;num--
;return num
;System.out.println(Thread.currentThread().getName()+"-------"+remain())
。這四個任務的共同點也是關鍵點在於它們都操做同一個資源Ticket對象中的num,這是多線程出現安全問題的本質,也是分析多線程執行過程的切入點。
當main線程開啓t1-t4這4個線程時,它們首先進入就緒隊列等待被CPU隨機選中。(1).假如t1被先選中,分配的時間片執行到任務②就結束了,因而t1進入就緒隊列等待被CPU隨機選中,此時票數num自減後爲99;(2).當t3被CPU選中時,t3所讀取到的num也爲99,假如t3分配到的時間片在執行到任務②也結束了,此時票數num自減後爲98;(3).同理t2被選中執行到任務②結束後,num爲97;(4).此時t3又被選中了,因而能夠執行任務③,甚至是任務④,假設執行完任務④時間片才結束,因而t3的打印語句打印出來的num結果爲97;(5).t1又被選中了,因而任務④打印出來的num也爲97。
顯然,上面的代碼有幾個問題:(1)有些票沒有賣出去了可是沒有記錄;(2)有的票重複賣了。這就是線程安全問題。
java中解決線程安全問題的方法是使用互斥鎖,也可稱之爲"同步"。解決思路以下:
(1).爲待執行的任務設定給定一把鎖,擁有相同鎖對象的線程在wait()時會進入同一個線程池睡眠。
(2).線程在執行這個設了鎖的任務時,首先判斷鎖是否空閒(即鎖處於釋放狀態),若是空閒則去持有這把鎖,只有持有這把鎖的線程才能執行這個任務。即便時間片到了,它也不是釋放鎖,只有wait()或線程結束時纔會安全地釋放鎖。
(3).這樣一來,鎖被某個線程持有時,其餘線程在鎖判斷後就繼續會線程池睡眠去了(或就緒隊列)。最終致使的結果是,(設計合理的狀況下)某個線程必定完整地執行完一個任務,其餘線程纔有機會去持有鎖並執行任務。
換句話說,使用同步線程,能夠保證線程執行的任務具備原子性,只要某個同步任務開始執行了就必定執行結束,且不容許其餘線程參與。
讓線程同步的方式有兩種,一種是使用synchronized(){}
代碼塊,一種是使用synchronized關鍵字修飾待保證同步的方法。
class Ticket implements Runnable {
private int num; //初始化票的數量
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
//售票
public void sale() {
synchronized(obj) { //使用同步代碼塊封裝須要保證原子性的代碼
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
}
//獲取剩餘票數
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
class Ticket implements Runnable {
private int num; //初始化票的數量
Ticket(int num){
this.num = num;
}
public synchronized void sale() { //使用synchronized關鍵字,方法變爲同步方法
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
//獲取剩餘票數
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
使用同步以後,if(num>0)
、num--
、return num
和print(num)
這4個任務就強制具備原子性。某個線程只要開始執行了if語句,它就必定會繼續執行直到執行完print(num),纔算完成了一整個任務。只有完成了一整個任務,線程纔會釋放鎖(固然,也可能繼續判斷while(true)並進入下一個循環)。
前面的示例中,同步代碼塊synchronized(obj){}中傳遞了一個obj的Object對象,這個obj能夠是任意一個對象的引用,這些引用傳遞給代碼塊的做用是爲了標識這個同步任務所屬的鎖。
而synchronized函數的本質實際上是使用了this做爲這個同步函數的鎖標識,this表明的是當前對象的引用。但若是同步函數是靜態的,即便用了static修飾,則此時this還沒出現,它使用的鎖是"類名.class"這個字節碼文件對象,對於java來講,這也是一個對象,並且一個類中必定有這個對象。
使用相同的鎖之間會互斥,但不一樣鎖之間則沒有任何影響。所以,要保證任務同步(原子性),這些任務所關聯的鎖必須相同。也所以,若是有多個同步任務(各自保證本身的同步性),就必定不能都使用同步函數。
例以下面的例子中,寫了兩個相同的sale()方法,而且使用了flag標記讓不一樣線程能執行這兩個同步任務。若是出現了多線程安全問題,則代表synchronized函數和同步代碼塊使用的是不一樣對象鎖。若是將同步代碼塊中的對象改成this後不出現多線程安全問題,則代表同步函數使用的是this對象。若是爲sale2()加上靜態修飾static,則將obj替換爲"Ticket.class"來測試。
class Ticket implements Runnable {
private int num; //初始化票的數量
boolean flag = true;
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
//售票
public void sale1() {
synchronized(obj) { //使用的是obj標識鎖
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){} //爲了確保num--和println()分開,加上sleep
System.out.println(Thread.currentThread().getName()+"===sale1==="+remain());
}
}
}
public synchronized void sale2() { //使用this標識鎖
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){}
System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain());
}
}
//獲取剩餘票數
public int remain() {
return num;
}
public void run(){
if(flag){
while(true) {
sale1();
}
} else {
while(true) {
sale2();
}
}
}
}
public class Mytest {
public static void main(String[] args) {
Ticket t = new Ticket(200);
//建立多個線程對象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//開啓多個線程使其執行任務
t1.start();
try{Thread.sleep(1);} catch (InterruptedException i){}
t.flag = false;
t2.start();
}
}
如下是執行結果中的一小片斷,出現了多線程安全問題。而若是將同步代碼塊中的obj改成this,則不會出現多線程安全問題。
Thread-0===sale1===197
Thread-1===sale2===========197
Thread-0===sale1===195
Thread-1===sale2===========195
Thread-1===sale2===========193
Thread-0===sale1===193
Thread-0===sale1===191
Thread-1===sale2===========191
單例餓漢式:
class Single {
private static final Single s = new Single();
private Single(){};
public static Single getInstance() {
return s;
}
}
單例懶漢式:
class Single {
private static Single s = null;
private Single(){};
public static getInstance(){
if(s==null) {
s = new Single();
}
return s;
}
}
當多線程操做單例餓漢式和懶漢式對象的資源時,是否有多線程安全問題?
class Demo implements Runnable {
public void run(){
Single.getInstance();
}
}
以上面的代碼爲例。當多線程分別被CPU調度時,餓漢式中的getInstance()返回的s,s是final屬性修飾的,所以隨便哪一個線程訪問都是固定不變的。而懶漢式則隨着不一樣線程的來臨,不斷new Single()
,也就是說各個線程獲取到的對象s是不一樣的,存在多線程安全問題。
只需使用同步就能夠解決懶漢式的多線程安全問題。例如使用同步方法。
class Single {
private static Single s = null;
private Single(){};
public static synchronized getInstance(){
if (s == null){
s = new Single();
}
return s;
}
}
這樣一來,每一個線程來執行這個任務時,都將先判斷Single.class這個對象標識的鎖是否已經被其餘線程持有。雖然解決了問題,但由於每一個線程都額外地判斷一次鎖,致使效率有所降低。能夠採用下面的雙重判斷來解決這個效率下降問題。
class Single {
private static Single s = null;
private Single(){};
public static getInstance(){
if (s == null) {
synchronized(Single.class){
if (s == null){
s = new Single();
}
return s;
}
}
}
}
這樣一來,當第一個線程執行這個任務時,將判斷s==null
爲true,因而執行同步代碼塊並持有鎖,保證任務的原子性。並且,即便在最初判斷s==null
後切換到其餘線程了,也沒有關係,由於總有一個線程會執行到同步代碼塊並持有鎖,只要持有鎖了就必定執行s= new Single()
,在這以後,全部的線程在第一階段的"s==null"判斷都爲false,從而提升效率。其實,雙重判斷的同步懶漢式的判斷次數和餓漢式的判斷次數幾乎相等。
最典型的死鎖是僵局問題,A等B,B等A,誰都不釋放,形成僵局,最後兩個線程都沒法執行下去。
例以下面的代碼示例,sale1()中,obj鎖須要持有this鎖才能完成任務總體,而sale2()中,this鎖須要持有obj鎖才能完成任務總體。當兩個線程都開始執行任務後,就開始產生死鎖問題。
class Ticket implements Runnable {
private int num;
boolean flag = true;
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
public void sale1() {
synchronized(obj) { //obj鎖
sale2(); //this鎖
}
}
public synchronized void sale2() { //this鎖
synchronized(obj){ //obj鎖
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){}
System.out.println(Thread.currentThread().getName()+"========="+remain());
}
}
}
//獲取剩餘票數
public int remain() {
return num;
}
public void run(){
if(flag){
while(true) {
sale1();
}
} else {
while(true) {
sale2();
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket(200);
//建立多個線程對象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//開啓多個線程使其執行任務
t1.start();
try{Thread.sleep(1);} catch (InterruptedException i){}
t.flag = false;
t2.start();
}
}
爲了不死鎖,儘可能不要在同步中嵌套同步,由於這樣很容易形成死鎖。
注:若您以爲這篇文章還不錯請點擊右下角推薦,您的支持能激發做者更大的寫做熱情,很是感謝!