1,進程定義:進程就是指正在執行的程序,怎樣查看正在執行的進程呢?我們在使用電腦的時候,其實就有多個正在執行的程序,通過Ctri+Alt+Del 組合鍵可以進入windows任務管理器查看進程,我們進入後會看到很多.exe,這些就是我們的電腦當前正在執行的程序,也就是一個個的進程。
每一個程序執行的都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元。
2,線程:是進程中一個獨立控制單元,控制着進程的執行。一個進程中至少有一個線程。我們之前寫的程序都是單線程的程序。我們在編譯java文件的時候,會啓動javac進程,啓動java命令時,會啓動JVM執行.class文件。這個進程中至少有一個負責線程的執行,這個線程運行的代碼就是main函數裏面的代碼,該線程稱之爲主線程。
注意:通常我們可以理解爲這樣的程序時單線程程序,實際上並不止就這一個線程。原因是還有一個線程就是JVM的垃圾回收機制中控制垃圾回收的線程,在主線程執行的時候會啓動,回收內存中不再使用的對象的內存,其實最少有兩個線程。
多線程最常見的應用就是下載軟件,下載軟件在下載東西的時候,就是多個線程同時向服務器發送請求,同時多條路勁在下載文件。其實多線程在執行的時候並不是多個線程同時執行,而是CPU在多個線程之間進行這快速的切換,中間的事件間隔我們可以忽略,因爲太快了,所以我們認爲是同時在執行,其實這種執行叫做併發執行。
二,線程的五種狀態:
(1)被創建:創建Thread類的子類,將要運行的代碼放在run方法中,調用start方法創建線程,並調用run方法,此時線程進入運行狀態。
(2)運行:運行狀態就是run方法中的代碼執行過程,調用stop方法終止整個線程,run方法結束。運行狀態時調用sleep方法或者wait方法,是線程進入(3)凍結狀態,此時線程放棄了執行權,當睡眠時間或者從凍結狀態調用notify方法,能從凍結狀態轉化爲運行狀態。
(4)阻塞:這個狀態比較特殊,這個狀態線程具有執行權,但是在等待CPU資源,這個狀態有可能在run中的代碼運行一部分還沒運行完時,CPU去執行其他線程中的代碼去了。凍結狀態被叫醒後不一定直接進入運行狀態,也有可能進入阻塞狀態。當然阻塞狀態也有可能進入凍結狀態。凍結狀態:沒有了執行權,當然某個線程睡眠或者等待的時候。
(5)消亡:也就是該線程結束,run中的代碼執行完。如果中途要關閉,則通過調用stop方法,否則線程執行完自動消亡。
三,自定義線程:
參考java文檔的Thread類時,發現:
創建新執行線程有兩種方法。一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。接下來可以分配並啓動該子類的實例。例如,計算大於某一規定值的質數的線程可以寫成:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然後,下列代碼會創建並啓動一個線程:
PrimeThread p = new PrimeThread(143);
p.start();
通過API的解釋可以看出創建一個線程的方式一繼承Thread類:
1,創建一個類,繼承Thread類
2,重寫Thread類中的run方法
3,調用線程的啓動方法,start,該方法的作用有兩個,一個是啓動線程和調用run方法。
示例一:
class RunDemo extends Thread {
public void run() {
for(int i=0;i<70;i++) {
System.out.println("Demo run ......");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunDemo d = new RunDemo();
d.start();
for(int i=0;i<70;i++) {
System.out.println("main run...");
}
}
}
這個程序在執行的時候應該是Demo run和main run是交替執行的。執行過程是主線程啓動,main函數執行,然後RunDemo線程啓動,run方法和main方法裏面的for循環併發執行。
4,爲什麼定義一個繼承Thread類的類的線程時候,要調用start方法,而不調用run方法呢?原因是如果調用了run方法,那麼就不是一個獨立的控制單元控制一段代碼塊的執行了,就成了方法的調用,程序中相當於只有一個main線程。程序會按順序執行。
示例二:定義一個線程類,然後在main方法中啓動兩個自定義線程,交替執行線程中的內容。
class RunDemo extends Thread {
//private String name;
RunDemo(String name) {
//this.name = name;
super(name);//調用父類的構造函數給自定義線程賦一個名字
}
public void run() {
for(int i=0;i<70;i++) {
System.out.println(Thread.currentThread().getName() + " run ......" + i);
//System.out.println(this.getName() + " run ......" + i);等價於上面這個
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunDemo d = new RunDemo("one");
RunDemo d1 = new RunDemo("two");
d.start();
d1.start();
/*for(int i=0;i<70;i++) {
System.out.println("main run...");
}*/
}
}
Thread.currentThread().getName()可以獲得線程對象的名字,通過setName或者構造函數可以設置名字,其他操作查看java文檔。
自定義線程方式二:
創建線程的另一種方法是聲明實現 Runnable 接口的類。該類然後實現 run 方法。然後可以分配該類的實例,在創建 Thread 時作爲一個參數來傳遞並啓動。採用這種風格的同一個例子如下所示:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然後,下列代碼會創建並啓動一個線程:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
實現Runnable接口的方式創建線程步驟:
1,定義一個類實現Runnable接口;
2,覆蓋Runnable接口中的run方法;目的:將線程要運行的代碼存放在該run方法中;
3,通過Thread類建立線程對象;
4,將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數;原因是:run方法是屬於Runnable接口的子類對象,所以要讓線程指定所指定對象的run方法,就必須明確run方法所屬的對象;
5,調用Thread類的run方法,啓動線程,並調用Runnable接口子類的run方法。
示例:
需求:模擬火車站窗口購票系統,一共100張票;
分析:利用多線程的原理,火車票多個窗口同時在賣一定數量的火車票,當窗口1賣了1號座位的車票,其他窗口就不能再賣1號座位的票了;那個窗口賣的是幾號座位的票取決於cpu的執行順序,多個窗口賣火車票,相當於多個線程在同時執行;如果使用方式一創建線程必定出問題,假設四個窗口,每個窗口都要創建一個Thread子類的對象,這樣一共就是400張票。如果只創建一個對象,讓該對象運行四次,那麼運行時必定會出現錯誤提示,線程狀態錯誤。所以使用這種方式創建時不可以的。解決方法是使用第二種創建線程的方式:
代碼如下:
class Demon2 implements Runnable {
private int tickets = 100;
Object obj = new Object();
public void run() {
while(true) {
if(tickets>0) {
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
//創建Runnable接口子類對象
Demon2 d = new Demon2();
//創建線程對象,將Runnable接口的子類對象傳給線程
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
Thread t4 = new Thread(d);
//啓動線程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
總結:實現方式和繼承方式的區別:
繼承Thread,線程代碼存放在Tread子類的的run方法中。
實現Runnable,線程代碼存放在接口的子類的run方法中。
實現的好處:避免了單線程的侷限性。定義線程的時候,第一種方式不建議使用。建議使用第二種方式。
四,線程的同步:
1,上述賣票系統,在判斷tickets之後讓該線程睡眠1秒鐘,這時候通過分析發現會打印出0,-1,-2,-3等錯誤座位,這就是多線程存在的安全問題。原因:當多條語句在操作同一個線程共享數據時,一個線程的多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行,導致共享數據的錯誤;
解決方法:
對多條操作共享數據的語句,只能讓一個線程執行完,在執行過程中,其他線程不可以參與進來執行;
2,Java對多線程的安全問題有專門的解決方法:就是同步代碼塊;
synchronized(對象) {
需要被同步的代碼塊
}
對象如同鎖,持有鎖的線程可以在同步中執行。沒有持有鎖的線程即使獲得cpu的執行權,也進不去,因爲沒有鎖;哪些代碼需要同步,就看哪些語句在操作共享數據;
3,同步的前提是:
(1)要有兩個或兩個以上的線程;(2)必須是多個線程使用同一個鎖;(3)必須保證同步中只能有一個線程在執行;
請看下面示例:
class Demon2 implements Runnable {
private int tickets = 100;
Object obj = new Object();
public void run() {
while(true) {
synchronized(obj) { //重點部分,加鎖了。每個線程進來之前都會判斷該鎖是否開啓,
//如果開啓就進入,然後將鎖關閉,這樣後來的線程就沒法進入,等之前進來的程序執行完後才能進來
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
Demon2 d = new Demon2();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
4,在函數上加鎖:
雖然鎖的方法有兩種,就是鎖住共享數據部分或者鎖住函數,但是這裏如果直接給函數上鎖的話,一旦一個線程進去之後就不能出來,所以不能直接在run函數上加鎖,要先將共享數據封裝到一個函數內部,然後多該封裝函數加鎖。
class Demon2 implements Runnable {
private int tickets = 1000;
//Object obj = new Object();
boolean flag = true;
public void run() {
if(flag) {
while(true) {
synchronized(this) { //如果改成自定義的obj,那麼這兩個進程使用的鎖就不是同一個鎖,不滿足同步的條件
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
else
while(true){
show();
}
}
public synchronized void show() {//這裏的鎖使用的對象時this
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
Demon2 d = new Demon2();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.flag = flase;
t2.start();
}
}
五,結合線程同步的單例設計模式。
//餓漢式
class Single {
private static final Single s = new Single();
private Single() {}
public static Single getInstance() {
return s;
}
}
//帶延遲加載的懶漢式
class Single2 {
private static Single2 s = null;
private Single2() {}
public static Single2 getInstance() {
if(s == null) {
synchronized (Single2.class) {
if(s == null) {
s = new Single2();
}
}
}
return s;
}
}
重點:面試中可能會問到:懶漢式和餓漢式的區別是什麼?回答:懶漢式的特點在於延遲加載。懶漢式的延遲加載有沒有什麼問題?回答:如果是多線程訪問時會出現安全問題。解決方法是用同步來解決。用同步代碼塊和同步函數都可以,但是效率比較低。用雙重判斷的方式能夠解決效率問題。同步的時候的鎖是屬於該類所屬的字節碼文件對象。
六,死鎖。
所謂的死鎖就是指,兩個進程各自拿着各自的鎖而不是放資源,而每個線程要想運行,就必須拿到對方的鎖,這時候就會出現死鎖的問題。
關於死鎖的示例:
class ThreadDead implements Runnable {
private boolean flag;
public ThreadDead(boolean flag) {
this.flag = flag;
}
public void run() {
while(true) {
if(flag) {
synchronized (MyLock.lock1) {
System.out.println("if lock1 run...");
synchronized (MyLock.lock2) {
System.out.println("if lock2 run...");
}
}
}
else {
synchronized (MyLock.lock2) {
System.out.println("else lock2 run...");
synchronized (MyLock.lock1) {
System.out.println("else lock1 run...");
}
}
}
}
}
}
class MyLock {
static Object lock1 = new Object();
static Object lock2 = new Object();
}
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDead(true));
Thread t2 = new Thread(new ThreadDead(false));
t1.start();
t2.start();
}
}