1.進程:正在進行中的 程序(直譯)(任務管理器中的都是進程)
分配程序的空間,不執行。
2.線程:就是進程中的一個負責程序執行的控制單元(執行路徑)
一個進程中可以有多個執行路徑,稱之爲多線程。
一個進程中至少要有一個線程。
開啓多個線程是爲了同時運行多個代碼。
每一個線程都有自己運行的內容。這個內容稱爲線程要執行的任務。
其實應用程序的運行都是CPU在做着快速切換完成的。這個切換是隨機的。
管理進程的運行:中央處理器CPU,快速切換執行的進程,看起來是同時執行。
(多核(CPU)電腦可以提高多進程運行速度,但是還是得看內存大小。)
2.1
多線程的好處:解決了多部分代碼同時運行的問題。
多線程的弊端:開多了效率低。
2.2 JVM中的多線程解析:
①主線程:肯定有一條線程在執行main函數。
該線程的任務代碼都定義在main函數中。
(主線程結束時,虛擬機不一定結束,還有其他線程)
②垃圾回收線程:還有一條線程在執行垃圾回收器。(其實每個對象都具備着被回收的方法,在Object.finalize(),這個方法由對象的垃圾回收器調用此方法,垃圾回收器在System.gc()靜態方法)
該線程的任務代碼定義在垃圾回收器中。
/*//讓對象被回收,但是不是立即
class Demo
{
public void finalize() //其實沒必要覆寫
{
System.out.println(“demo ok”);
}
}
class ThreadDemo
{
public static void main(String[] args) //這是一個線程
{
new Demo();
new Demo();
System.gc(); //不是立即執行,這又是另一個線程,執行具有不確定性。
new Demo();
System.out.println(“HAHA”);
}
}
*/
等等。
2.3主線程運行示例:
3.多線程存在的意義
4.線程的創建方式
1)創建方式一:繼承Thread類
步驟:
1.定義一個類繼承Thread類。
2.覆蓋Thead類中的run方法:
自定義的線程的任務通過Thread類中的run方法來體現。也就是說,run方法就是封裝自定義線程運行任務的函數。
run方法中定義的就是線程要運行的任務代碼。
開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並複寫run方法。
將運行的代碼定義在run方法中即可。
3.直接創建Thread類的子類對象。
4.調用start方法開啓線程並調用線程的任務run方法執行。
調用run和調用start的區別:
run:調用要在線程中執行的任務(代碼)。
start:開啓線程並調用線程的任務run方法執行。
代碼:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name = name;
}
public void run()
{
int[] arr = new int[3];
System.out.println(arr[3]); ////發生異常,線程終止
show();
}
public void show()
{
for (int x = 0;x < 30 ;x++ )
{
// for (long y = 0; y <= 1000000000l;y++ ){} //long類型延遲比int明顯
System.out.println(name+ "........x=" + x +".....name=" + Thread.currentThread().getName()); //getName()獲取線程的名字(Thread-數字(從0開始))(但線程可能沒有運行)。
//要獲取運行時線程的名字,就得先通過靜態方法Thread.currentThread()獲得對當前正在執行的線程對象的引用。
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d1 =new Demo("旺財"); //創建對象的時候就已經完成了線程名稱的定義,所以.run()也會輸出線程名稱。
/* 源碼: public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
*/
Demo d2 = new Demo("xiaoqiang");
d1.run(); //調用要在線程中執行的任務(代碼),和正常調用沒區別。
//這時返回的運行時線程名是 main
// d1.start(); //創建並啓動線程。(使該線程開始執行;Java 虛擬機調用該線程的 run 方法。)
d2.start();
System.out.println(4/0); //發生算數異常,線程終止
System.out.println("over"); //主線程,這時一共由三個線程,隨機執行
}
}
Thread類中的方法和名稱:
方法:
1.getName():獲取線程的名字(Thread-數字)(從0開始)(但線程可能沒有運行)。
2.Thread.currentThread():靜態方法,獲得對當前正在執行的線程對象的引用。
線程的名稱:
1.主函數的線程名爲main
2.線程名自定義:
在子類的初始化函數中第一行添加super(name),name爲自定義線程名。其實就是調用了Thread類的構造方法。
5.多線程運行圖解:
1.線程之間獨立運行,都有自己的運行空間,相互不影響。
2.線程發生異常立即跳棧,不再執行,不影響其他線程的執行。
主線程掛了,其他線程也不會立即結束,除非虛擬機關閉。
6.線程的狀態:
CPU的執行資格:可以被CPU處理,在處理隊列中排隊
CPU的執行權:正在被CPU處理
7.線程創建的第二種方式:實現Runnable接口
步驟:
1.定義類實現Runnable接口。
2.覆蓋接口中的run方法,將線程的任務代碼封裝到run方法中。
3.通過Thread類創建線程對象,並將Runnable接口的子類對象作爲構造函數的參數進行傳遞。
爲什麼:因爲線程的任務都封裝在Runnable接口的子類對象的run方法中,
所以要在線程對象創建時就必須明確要運行的任務。
4.調用線程對象的start方法開啓線程。
原因:
創建方式一,繼承Thread類。當需要線程的類中已經繼承了其他類,就不能再繼承Thread類了。
這個時候還要用到額外的方法創建就需要使用接口。而Thread類已經實現了Runnable接口,它默認run方法就是覆蓋的Runnable接口的run抽象方法,不執行任何操作。並且Thread類提供了可以帶Runnable接口對象的構造方法,來分配新的 Thread 對象,調用新對象的run方法。
內部實現的思想細節:
class Thread implements Runnable
{
private Runnable r;
Thread()
{
}
Thread(Runnable r) //第二種創建方式
{ //在新的Thread對象中覆蓋Runnable的run方法。
this.r = r;
}
public void run()
{
if(r!=null)
r.run();
}
public void start()
{
run();
}
}
class SubThread extends Thread
{ //第一種創建方式
public void run() //覆蓋Thread中的run()
{
}
}
SubThread s =new SubThread();
s.start(); //調用的是父類的start()和子類覆蓋的run()
class ThreadImpl implements Runnable
{
public void run(); //第二種創建方式
}
Thread t = new Thread(new ThreadImpl());
t.start();
API說明:
Thread
public Thread(Runnable target)
分配新的 Thread 對象。這種構造方法與 Thread(null, target,gname) 具有相同的作用,其中的 gname 是一個新生成的名稱。自動生成的名稱的形式爲 “Thread-”+n,其中的 n 爲整數。
代碼:
class Demo implements Runnable
{
public void run()
{
show();
}
void show()
{
for (int x = 0; x <20 ;x++ )
{
System.out.println(Thread.currentThread().getName() + "......" + x);
}
}
}
class ThreadDemo3
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Demo()); //Thread類是在構造方法中定義的是Runable接口中自己覆蓋的run方法,
//即類Thread實現了Runable接口
/*API解釋:
run
public void run()
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。
Thread 的子類應該重寫該方法。
*/
//所以賦值一個實現Runable接口的子類,調用的就是子類覆蓋的的run方法。
Thread t2 = new Thread(new Demo());
t1.start(); //開啓線程並調用線程中的run方法執行
t2.start();
}
}
第二種方式的好處:
第一種方式,繼承了Thread,會變成Thread體系中的一員,具備了Thread類中的所有方法。但是用類描述事物完了後,如果僅僅只是需要將一部分代碼被對線程所操作的話,就沒有必要去具備線程對象中的所有方法。做繼承的目的僅僅是爲了覆蓋run方法,建立線程運行的任務。
所以有了第二種方式,實現Runnable接口,它的出現僅僅是將線程的任務進行了對象的封裝,不需要線程出現。如果需要有多線程,就去實現Runnable接口,把任務代碼封裝在run方法中,變成Runnable接口的子類對象,就是線程任務對象。(這是一種思想的變化)
Runnable r = new Student();
要運行多線程時,直接創建Thread對象,在創建對象的同時,明確線程任務對象就行了。
Thread t = new Thread(r);
t.start();
Thread類和其他需要多線程的類中都存在run()方法,向上抽取,而線程是事物的一個額外功能,所以抽取並實現了Runnable接口。
實現Runnable的好處:
1.將線程的任務從線程的子類中分離出來,進行了單獨的封裝。
按照面向對象的思想將任務封裝成對象。
2.避免了Java單繼承的侷限性。
所以,創建線程的第二種方式較爲常見。
8.多線程小例子:
/*
賣票示例:
需求:四個窗口同時賣票
問題:
1.票數共用 : 就只能使用一個對象,而繼承方式需要創建四個對象,所以用接口定義方式創建線程。
2.保證每次買票過程能完成:
就是將多條操作貢獻數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。
必須要當前線程把這些代碼都執行完畢後,其他線程纔可以參與運算。
在Java中,用同步代碼塊姐可以解決這個問題。
同步代碼塊的格式:
synchronized(對象)
{
需要被同步的代碼;
}
多次啓用一個線程是非法的。
*/
class Ticket implements Runnable // extends Thread //繼承無法實現同時賣100張票完成。
{
private int num = 100; //靜態化後,對象就沒有意義。而且要賣另外不同的100張票呢。
Object obj = new Object(); //保證用的是同一個鎖
public void run()
{
// Object obj = new Object(); //這樣會有4把鎖,同步會失效
sale();
}
public void sale() //不能拋出異常,因爲實現的接口沒有拋
{
while(true)
{
//同步代碼塊
synchronized(obj)
{
if(num >0)
{
/* try
{
Thread.sleep(10); //線程安全問題測試
}
catch (InterruptedException e)
{
}
*/
System.out.println(Thread.currentThread().getName() + ":餘票:"+ --num);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket(); //創建一個線程任務對象,火車票
Ticket tt = new Ticket(); //賣第二種票,動車票
Thread t1 = new Thread(t,"窗口1(火車票)");
Thread t2 = new Thread(t,"窗口2(火車票)");
Thread t3 = new Thread(tt,"窗口3(動車票)"); //窗口3,4 買動車票
Thread t4 = new Thread(tt,"窗口4(動車票)");
t1.start();
t2.start();
t3.start();
t4.start();
// t1.start();
// t1.start(); //這裏會導致 主線程 發生線程狀態異常。
// t1.start();
// t1.start();
}
}
問題分析:
線程安全問題的現象:
if(num >0)
System.out.println...
當num = 1 的時候:
第一個線程剛剛進去if循環準備執行輸出語句的時候,cpu不調用它了,使其進入臨時阻塞狀態。
然後cpu調用線程二進來執行輸出語句,輸出1,再--,0。
這時cpu回來調用線程一,這個時候不需要判斷直接執行了輸出語句,輸出0,再--,num的值爲 -1。顯然0號票不合理。
產生安全問題的原因:
(因爲num是共享的數據,多個線程操作時相互之間可能有影響。)
1.多個線程在操作共享數據。
(如果只有一個語句,不會出事。
兩條以上可能導致問題,因爲可能在結束一條語句時,其他線程進來了,使當前線程暫停進入臨時阻塞狀態。而等其他線程操作完,再回來執行該線程的第二條語句時,共享數據可能就已經發生了改變。)
2.操作共享數據的線程代碼有多條。
當一個線程在執行操作共享數據的多條代碼過程中,其他線程參與了運算,就會導致線程安全問題的產生。
解決問題:同步代碼塊
就是將多條操作貢獻數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。
必須要當前線程把這些代碼都執行完畢後,其他線程纔可以參與運算。
在Java中,用同步代碼塊姐可以解決這個問題。
同步代碼塊的格式:
synchronized(對象)
{
需要被同步的代碼;
}
同步的好處和弊端:
原理:在同步代碼塊中,當第一個線程進來時,獲取obj對象,開始執行代碼。這時沒有obj對象,其他線程無法進來,只有等到當前進程結束後四方obj對象。obj對象像個鎖,就叫對象鎖(同步鎖)。另外當前線程是不會一直有執行權的,只是切其他線程時它們進不來,只有再次切到當前線程,直到當前線程執行結束。
好處:解決了線程的安全問題。
弊端:相對降低了效率,因爲同步外的線程都會判斷同步鎖。
同步的前提:
同步中必須有多個線程,並使用的是同一個鎖。
9.同步函數:同步的第二種表現形式
在函數聲明中添加修飾符synchronized即可。
/*
需求:儲戶,兩個,每個都到銀行存錢,每次存100,共存三次。
*/
class Bank
{
private int sum; //共享數據
// private Object obj = new Object(); //唯一對象鎖
public synchronized void add(int num)
{
// synchronized(obj){ //這就是函數的全部代碼,直接定義同步函數
sum = sum +num;
//這裏會出現安全隱患。
System.out.println("sum = " + sum);
//}
}
}
class Cus implements Runnable
{
Bank b = new Bank();
public void run()
{
for (int x = 0; x < 3 ; x++ )
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
驗證同步函數的鎖:
驗證結果是不是同一個鎖,即同步函數鎖用的不是obj。
而是調用該方法的對象,this
注意:
但是當同步函數設置成靜態的時候,對象鎖就是這個方法所屬類的字節碼對象。getClass()可以獲取該對象所屬類的的字節碼對象,而字節碼對象是Class類的對象。
就是同步代碼塊中寫this.getClass()可以獲得和靜態同步函數一樣的同步鎖。
另一種表示方式:類名.class;也是返回所屬類的字節碼對象。class是Class類的一個靜態屬性,而所有的類在加載時都是以Class類的對象形式加載的。
API中說明:
getClass:
返回的 Class 對象是由所表示類的 static synchronized 方法鎖定的對象。
同步函數和同步代碼塊的區別:
同步函數的鎖是固定的this。
同步代碼塊的鎖是任意的對象(自定義,也不一定是obj)。
開發建議使用同步代碼塊。同步函數可以寫成同步代碼塊的簡寫形式。一簡寫,就有前提(如果用的鎖是this),有好處,有弊端。
驗證代碼:
/*
同步函數鎖驗證:
爲了驗證同步函數和同步代碼塊的鎖,
窗口一設置同步函數,
窗口二設置同步代碼塊,如果是同一個對象鎖,則不會輸出負值。
結果是不是同一個鎖,即同步函數鎖用的不是obj。
而是調用該方法的對象,this
*/
class Ticket implements Runnable // extends Thread //繼承無法實現同時賣100張票完成。
{
private int num = 100; //靜態化後,對象就沒有意義。而且要賣另外不同的100張票呢。
private Object obj = new Object(); //驗證用
boolean flag = true;
public void run()
{
sale();
}
// public synchronized void sale()
public void sale()
{
if(flag)
{
while(true) //同步函數sale後,第一個線程進來就出不去了,着是死循環。所以不應該同步sale函數。
{ //解決方法:將死循環裏的代碼封裝成同步函數即可。
show(); //窗口一設置同步函數
}
}
else //窗口二設置同步代碼塊,如果是同一個對象鎖,則不會輸出負值。
{
while(true)
{
synchronized(obj) //改成synchronized(this),就不會出現錯誤值
//如果同步函數是靜態的,則改成synchronized(this.getClass()),通過getClass()獲得本類的 static synchronized 方法鎖定的對象。
//或者synchronized(Ticket.class),等效
{
if(num >0)
{
try{Thread.sleep(10); }catch (InterruptedException e){} //線程安全問題測試
System.out.println(Thread.currentThread().getName() + ":餘票(obj):"+ --num);
}
}
}
} }
private synchronized void show() //同步鎖不是obj,是this,即new Ticket()。
//如果改成靜態的,則同步鎖對象就是所屬類的字節碼對象。
{
if(num >0)
{
try{Thread.sleep(10); }catch (InterruptedException e){} //線程安全問題測試
System.out.println(Thread.currentThread().getName() + ":餘票:(function)"+ --num);
}
}
}
class SynFunctionLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket(); //創建一個線程任務對象,火車票
Thread t1 = new Thread(t,"窗口1"); //爲了驗證同步函數和同步代碼塊的鎖。設置同步函數
Thread t2 = new Thread(t,"窗口2");//設置同步代碼塊
t1.start();
//這裏爲了讓線程一能夠有完成 flag =true的賦值動作,讓主線程暫停一下。
try{Thread.sleep(100); }catch (InterruptedException e){} //線程安全問題測試
t.flag = false;
t2.start();
}
}
10.單例模式涉及的多線程問題。
關於懶漢式單例設計模式在多線程中的
面試問題:
1.安全麼? 不安全
2.寫個同步函數,效率高麼?同步鎖是哪一個?怎麼解決?
不高,因爲每次獲取對象都要判斷同步鎖。
靜態同步函數的同步鎖是所在類的字節碼對象,可以通過類名.class來獲取。
解決辦法:通過同步代碼塊把創建對象的代碼封裝起來,再放到對象是否爲空的判斷裏,這樣就只用判斷一次同步鎖,提高效率。
/*
多線程下的單例:
*/
//餓漢式
class Single
{
private Single(){}
private static Single s = new Single();
public static void getInstance()
{
return s; //一句話,不存在安全隱患。
}
}
//懶漢式
class Single
{
private Single(){}
private static Single s =null;
public static/* synchronized*/ getInstance() //解決辦法1:加同步函數,但是每次拿對象都要判斷同步鎖,效率低。
{
if(s==null) //解決辦法2:通過同步代碼塊把創建對象的代碼封裝起來,再放到判斷裏,這樣就只用判斷一次同步鎖。
{ //加判斷是解決效率問題
synchronized(Single.class) //加同步是解決安全問題
{
if(s == null)
//線程0進來,切換到線程1,線程1暫停,再切到線程0,建立返回一個對象,
//然後切換到線程1,不用判斷,又建立一個對象。此時無法保證對象的唯一性了。存在安全隱患。
s = new Single();
}
}
reruen s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
11.死鎖示例:
/*
死鎖:常見情景之一:同步嵌套
*/
class Ticket implements Runnable
{
private int num = 100;
private Object obj = new Object();
boolean flag = true;
public void run()
{
sale();
}
public void sale()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
show();
}
}
}
else
{
while(true)
{
show(); //線程二拿着this鎖進obj鎖,同時發生導致程序死鎖
}
}
}
private synchronized void show() //線程一拿着obj鎖進this鎖,同時發生導致程序死鎖
{
synchronized(obj)
{
if(num >0)
{
try{Thread.sleep(10); }catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ":餘票(obj):"+ --num);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
t1.start();
try{Thread.sleep(100); }catch (InterruptedException e){}
t.flag = false;
t2.start();
}
}
死鎖程序:面試用
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true) //增加死鎖概率
{
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+ "...if locka...");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"...if lockb...");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"...else lockb...");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"...else locka...");
}
}
}
}
}
}
class MyLock
{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Test a = new Test(true); //因爲是boolean型變量,值是固定的 。所以多個內容沒有影響
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}