java基礎(五):談談java中的多線程

1.多線程

1.1.多線程介紹

  學習多線程以前,咱們先要了解幾個關於多線程有關的概念。html

  進程:正在運行的程序。確切的來講,當一個程序進入內存運行,即變成一個進程,進程是處於運行過程當中的程序,而且具備必定獨立功能,進程是系統進行資源分配和調度的一個獨立單位。進程是正在運行的程序,進程負責給程序分配內存空間,而每個進程都是由程序代碼組成的,這些代碼在進程中執行的流程就是線程。java

  線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序。編程

  簡而言之:一個程序運行後至少有一個進程,一個進程中能夠包含多個線程,但至少有一個線程。什麼是多線程呢?即就是一個程序中有多個線程在同時執行。安全

1.2.多線程運行原理

  大部分操做系統都支持多進程併發運行,如今的操做系統幾乎都支持同時運行多個任務。好比:如今咱們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。感受這些軟件好像在同時運行着。bash

  其實這些軟件在某一時刻,只會運行一個進程。這是爲何呢?這是因爲CPU(中央處理器)在作着高速的切換而致使的。對於CPU而言,它在某個時間點上,只能執行一個程序,即就是說只能運行一個進程,CPU不斷地在這些進程之間切換。只是咱們本身感受不到。爲何咱們會感受不到呢?這是由於CPU的執行速度相對咱們的感受實在太快了,雖然CPU在多個進程之間輪換執行,但咱們本身感到好像多個進程在同時執行。服務器

  多線程真的能提升效率嗎?其實並非這樣的,由於咱們知道,CPU會在多個進程之間作着切換,若是咱們開啓的程序過多,CPU切換到每個進程的時間也會變長,咱們也會感受機器運行變慢。因此合理的使用多線程能夠提升效率,可是大量使用,並不能給咱們帶來效率上的提升。多線程

1.3.主線程

  回想咱們之前學習中寫過的代碼,當咱們在dos命令行中輸入java空格類名回車後,啓動JVM,而且加載對應的class文件。虛擬機並會從main方法開始執行咱們的程序代碼,一直把main方法的代碼執行結束。若是在執行過程遇到循環時間比較長的代碼,那麼在循環以後的其餘代碼是不會被執行的。以下代碼演示:併發

class Demo
{
    String name;
    Demo(String name)
    {
        this.name = name;
    }
    void show()
    {
        for (int i=1;i<=20 ;i++ )
        {
            System.out.println("name="+name+",i="+i);
        }
    }
}
class ThreadDemo 
{
    public static void main(String[] args) 
    {
        Demo d = new Demo("小強");
        Demo d2 = new Demo("旺財");
        d.show();        
        d2.show();
        System.out.println("Hello World!");
    }
}
複製代碼

  若在上述代碼中show方法中的循環執行次數不少,這時書寫在d.show();下面的代碼是不會執行的,而且在dos窗口會看到不停的輸出name=小強,i=值,這樣的語句。爲何會這樣呢?dom

  緣由是:jvm啓動後,必然有一個執行路徑(線程)從main方法開始的。一直執行到main方法結束。這個線程在java中稱之爲主線程。當主線程在這個程序中執行時,若是遇到了循環而致使程序在指定位置停留時間過長,沒法執行下面的程序。jvm

  可不能夠實現一個主線程負責執行其中一個循環,由另外一個線程負責其餘代碼的執行。實現多部分代碼同時執行。這就是多線程技術能夠解決的問題。

1.4.如何建立線程

1.4.1.建立線程方式一:繼承Thread類

  該如何建立線程呢?經過API中的英文Thread的搜索,查到Thread類。經過閱讀Thread類中的描述。建立新執行線程有兩種方法。一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。接下來能夠分配並啓動該子類的實例。

  建立線程的步驟:

  1. 定義一個類繼承Thread。

  2. 重寫run方法。

  3. 建立子類對象,就是建立線程對象。

  4. 調用start方法,開啓線程並讓線程執行,同時還會告訴jvm去調用run方法。

class Demo extends Thread  //繼承Thread
{
    String name;
    Demo(String name)
    {
        this.name = name;
    }
    //複寫其中的run方法
    public void run()
    {
        for (int i=1;i<=20 ;i++ )
        {
            System.out.println("name="+name+",i="+i);
        }
    }
}
class ThreadDemo 
{
    public static void main(String[] args) 
    {
        //建立兩個線程任務
        Demo d = new Demo("小強");
        Demo d2 = new Demo("旺財");
        //d.run(); 這裏仍然是主線程在調用run方法,並無開啓兩個線程
        //d2.run();
        d2.start();//開啓一個線程
        d.run();//主線程在調用run方法
    }
}
複製代碼

  打印部分結果:因爲多線程操做,輸出數據會有所不一樣

    name=旺財,i=1

    name=小強,i=1

    name=旺財,i=2

    name=小強,i=2

    name=小強,i=3

    name=旺財,i=3

    name=旺財,i=4

    name=旺財,i=5

    name=旺財,i=6

    name=旺財,i=7

    ..........

  思考:線程對象調用 run方法和調用start方法區別?

線程對象調用run方法不開啓線程。僅是對象調用方法。線程對象調用start開啓線程,並讓jvm調用run方法在開啓的線程中執行。

1.4.2.繼承Thread類原理

  爲何要繼承Thread類,並調用其的start方法才能開啓線程呢?

  繼承Thread類:由於Thread類描述線程事物,具有線程應該有功能。

  那爲何不直接建立Thread類的對象呢?

1 Thread t1 = new Thread();
2 t1.start();//這樣作沒有錯,可是該start調用的是Thread類中的run方法,而這個run方法沒有作什麼事情,更重要的是這個run方法中並無定義咱們須要讓線程執行的代碼。
複製代碼

  建立線程的目的是什麼?

  是爲了創建單獨的執行路徑,讓多部分代碼實現同時執行。也就是說線程建立並執行須要給定的代碼(線程的任務)。對於以前所講的主線程,它的任務定義在main函數中。自定義線程須要執行的任務都定義在run方法中。Thread類中的run方法內部的任務並非咱們所須要,只有重寫這個run方法,既然Thread類已經定義了線程任務的位置,只要在位置中定義任務代碼便可。因此進行了重寫run方法動做。

1.5.多線程的內存圖解

  多線程執行時,到底在內存中是如何運行的呢?

  以上個程序爲例,進行圖解說明:

  多線程執行時,在棧內存中,其實每個執行線程都有一片本身所屬的棧內存空間。進行方法的壓棧彈棧

  當執行線程的任務結束了,線程自動在棧內存中釋放了。可是當全部的執行線程都結束了,那麼進程就結束了。

1.6.獲取線程名稱

  開啓的線程都會有本身的獨立運行棧內存,那麼這些運行的線程的名字是什麼呢?該如何獲取呢?既然是線程的名字,按照面向對象的特色,是哪一個對象的屬性和誰的功能,那麼咱們就去找那個對象就能夠了。查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,咱們就能夠試試。

   Thread.currentThread()獲取當前線程對象

   Thread.currentThread().getName();獲取當前線程對象的名稱

class Demo extends Thread  //繼承Thread
{
    String name;
    Demo(String name)
    {
        this.name = name;
    }
    //複寫其中的run方法
    public void run()
    {
        for (int i=1;i<=20 ;i++ )
        {
            System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
        }
    }
}
class ThreadDemo 
{
    public static void main(String[] args) 
    {
        //建立兩個線程任務
        Demo d = new Demo("小強");
        Demo d2 = new Demo("旺財");
        d2.start();//開啓一個線程
        d.run();//主線程在調用run方法
    }
}
複製代碼

原來主線程的名稱: main

自定義的線程: Thread-0 線程多個時,數字順延。Thread-1......

進行多線程編程時不要忘記了Java程序運行時從主線程開始,main方法的方法體就是主線程的線程執行體。

1.7.建立線程的第二種方式

  掌握瞭如何建立線程對象,以及開啓線程後,記得在查閱API時,還說了有第二種開啓線程的方式,那麼第二種是什麼呢?

1.7.1.實現Runnable接口

  繼續查看API發現,建立線程的另外一種方法是聲明實現 Runnable 接口的類。該類而後實現 run 方法。而後能夠分配該類的實例,在建立 Thread 時做爲一個參數來傳遞並啓動。

  怎麼還要實現Runnable接口,Runable是啥玩意呢?繼續API搜索。

  查看Runnable接口說明文檔:Runnable 接口應該由那些打算經過某一線程執行其實例的類來實現。類必須定義一個稱爲 run 的無參數方法。

  總結:

  建立線程的第二種方式:實現Runnable接口。

   一、定義類實現Runnable接口。

   二、覆蓋接口中的run方法。。

   三、建立Thread類的對象

   四、將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。

   五、調用Thread類的start方法開啓線程。

代碼演示:
class Demo implements Runnable
{
    private String name;
    Demo(String name)
    {
        this.name = name;
    }
    //覆蓋了接口Runnable中的run方法。
    public void run()
    {
        for(int i=1; i<=20; i++)
        {            System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
        }
    }
}
class ThreadDemo2 
{
    public static void main(String[] args) 
    {
        //建立Runnable子類的對象。注意它並非線程對象。
        Demo d = new Demo("Demo");
        //建立Thread類的對象,將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        //將線程啓動。
        t1.start();
        t2.start();
        System.out.println(Thread.currentThread().getName()+"----->");
        System.out.println("Hello World!");
    }
}
複製代碼

  輸出結果:

1.7.2.實現Runnable的原理

  爲何須要定一個類去實現Runnable接口呢?繼承Thread類和實現Runnable接口有啥區別呢?

  實現Runnable接口,避免了繼承Thread類的單繼承侷限性。覆蓋Runnable接口中的run方法,將線程任務代碼定義到run方法中。建立Thread類的對象,只有建立Thread類的對象才能夠建立線程。線程任務已被封裝到Runnable接口的run方法中,而這個run方法所屬於Runnable接口的子類對象,因此將這個子類對象做爲參數傳遞給Thread的構造函數,這樣,線程對象建立時就能夠明確要運行的線程的任務。

1.7.3.實現Runnable的好處

  第二種方式實現Runnable接口避免了單繼承的侷限性,因此較爲經常使用。實現Runnable接口的方式,更加的符合面向對象,線程分爲兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一塊兒。一旦建立Thread類的子類對象,既是線程對象,有又有線程任務。實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。

1.8.線程狀態圖

  查閱API關於IllegalThreadStateException這個異常說明信息發現,這個異常的描述信息爲:指示線程沒有處於請求操做所要求的適當狀態時拋出的異常。這裏面說適當的狀態,啥意思呢?難道是說線程還有狀態嗎?

  一、新建(new):線程對象被建立後就進入了新建狀態。如:Thread thread = new Thread();  

  二、就緒狀態(Runnable):也被稱爲「可執行狀態」。線程對象被建立後,其餘線程調用了該對象的start()方法,從而啓動該線程。如:thread.start(); 處於就緒狀態的線程隨時可能被CPU調度執行。

  三、運行狀態(Running):線程獲取CPU權限進行執行。須要注意的是,線程只能從就緒狀態進入到運行狀態。

  四、阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權限,暫時中止運行。直到線程進入就緒狀態,纔有機會進入運行狀態。阻塞的三種狀況:

  • 等待阻塞:經過調用線程的wait()方法,讓線程等待某工做的完成。
  • 同步阻塞:線程在獲取synchronized同步鎖失敗(由於鎖被其餘線程佔用),它會進入同步阻塞狀態。
  • 其餘阻塞:經過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或超時、或者I/O處理完畢時,線程從新轉入就緒狀態。

  五、死亡狀態(Dead):線程執行完了或因異常退出了run()方法,該線程結束生命週期。

1.8.1.sleep,wait,yield,join的區別

  • sleep()方法

    在指定時間內讓當前正在執行的線程暫停執行,但不會釋放「鎖標誌」。不推薦使用。 sleep()使當前線程進入阻塞狀態,在指定時間內不會執行。

  • wait()方法

    在其餘線程調用對象的notify或notifyAll方法前,致使當前線程等待。線程會釋放掉它所佔有的「鎖標誌」,從而使別的線程有機會搶佔該鎖。 當前線程必須擁有當前對象鎖。若是當前線程不是此鎖的擁有者,會拋出IllegalMonitorStateException異常。 喚醒當前對象鎖的等待線程使用notify或notifyAll方法,也必須擁有相同的對象鎖,不然也會拋出IllegalMonitorStateException異常。 waite()和notify()必須在synchronized函數或synchronized block中進行調用。若是在non-synchronized函數或non-synchronized block中進行調用,雖然能編譯經過,但在運行時會發生IllegalMonitorStateException的異常。

  • yield方法

    暫停當前正在執行的線程對象。 yield()只是使當前線程從新回到可執行狀態,因此執行yield()的線程有可能在進入到可執行狀態後立刻又被執行。 yield()只能使同優先級或更高優先級的線程有執行的機會。 調用yield方法並不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態,它只須要等待從新獲取CPU執行時間,這一點是和sleep方法不同的。

  • join方法

    等待該線程終止。 等待調用join方法的線程結束,再繼續執行。如:t.join();//主要用於等待t線程運行結束,若無此句,main則會執行完畢,致使結果不可預測

1.9.線程的安全問題

  帶女友看電影,須要買票,電影院要賣票,模擬電影院的買票操做

  假設咱們想要的電影是 「功夫熊貓3」,本次電影的座位共100個(本廠電影只能賣100張票)

  模擬電影院的售票窗口,實現多個窗口同時賣 「功夫熊貓3」這場電影票(多個窗口一塊兒賣這100張票)

  須要窗口:採用線程對象

  須要票:Runnable接口子類來模擬

public class ThreadDemo {
    public static void main(String[] args) {
        //建立票對象
        Ticket ticket = new Ticket();
        
        //建立3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    @Override
    public void run() {
        //模擬賣票
        while(true){
            //t1,t2,t3
            if (ticket > 0) {
                //模擬選坐的操做
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }
}
複製代碼

  總結:上面程序出新了問題

   票出現了重複的票

   錯誤的票 0

1.10.同步的鎖

  • 同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {     
    可能會產生線程安全問題的代碼
}
複製代碼

  同步代碼塊中的鎖對象能夠是任意的對象,多個線程對象使用的是同一個鎖對象

  把Ticket.java進行了代碼修改

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    
    //定義所對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步代碼塊
            synchronized (lock){
                if (ticket > 0) {
                    //模擬選坐的操做
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}
複製代碼

  當使用了同步代碼塊後,上述的線程的安全問題,解決了。

  • 同步方法:在方法聲明上加上synchronized
public synchronized void method(){
 
     可能會產生線程安全問題的代碼
 
 }
複製代碼

  同步方法中的鎖對象是 this

//同步方法,鎖對象this
    public synchronized void method(){
        //this.name = name;
        if (ticket > 0) {
            //模擬選坐的操做
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
複製代碼
  • 靜態同步方法: 在方法聲明上加上synchronized
public static synchronized void method(){
 可能會產生線程安全問題的代碼
 }
複製代碼

1.11.死鎖

  同步鎖的另外一個弊端:當線程任務中出現了多個同步(多個鎖)時,若是同步中嵌套了其餘的同步。這時容易引起一種現象:死鎖。這種狀況能避免就避免掉。

synchronzied(A鎖){
     synchronized(B鎖){
          
 }
 }
複製代碼
/*
 * 定義鎖對象
 */
public class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}

/*
 * 線程任務類
 */
public class ThreadTask implements Runnable {

    int x = new Random().nextInt(1);//0,1    
    
    //指定線程要執行的任務代碼
    @Override
    public void run() {
        while(true){
            if (x%2 ==0) {
                //狀況一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if大口吃肉");
                    }
                }
            } else {
                //狀況二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else大口吃肉");
                    }
                }
            }
            x++;
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        //建立線程任務類對象
        ThreadTask task = new ThreadTask();
        
        //建立兩個線程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        
        //啓動線程
        t1.start();
        t2.start();
    }
}
複製代碼

1.12.Lock接口

  查閱API,發現Lock接口,比同步更厲害,有更多操做;

   lock():獲取鎖

   unlock():釋放鎖;

  提供了一個更加面對對象的鎖,在該鎖中提供了更多的顯示的鎖操做。使用Lock接口,以及其中的lock()方法和unlock()方法替代同步。

  以下代碼演示:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    
    //建立Lock鎖對象
    Lock ck = new ReentrantLock();
    
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //synchronized (lock){
            ck.lock();
                if (ticket > 0) {
                    //模擬選坐的操做
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            ck.unlock();
            //}
        }
    }
}
複製代碼

1.13.線程的匿名內部類的使用

方式1
        new Thread() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...X...." + x);
                }
            }
        }.start();

方式2
        Runnable r = new Runnable() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...Y...." + x);
                }
            }
        };
        new Thread(r).start();
複製代碼

2.多線程文件上傳

  實現服務器端能夠同時接收多個客戶端上傳的文件。 咱們要修改服務器端代碼

/*
 * 文件上傳  服務器端
 *
 */
public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1,建立服務器,等待客戶端鏈接
        ServerSocket serverSocket = new ServerSocket(6666);
        
        //實現多個客戶端鏈接服務器的操做
        while(true){
            final Socket clientSocket = serverSocket.accept();
            //啓動線程,完成與當前客戶端的數據交互過程
            new Thread(){
                public void run() {
                    try{
                        //顯示哪一個客戶端Socket鏈接上了服務器
                        InetAddress ipObject = clientSocket.getInetAddress();//獲得IP地址對象
                        String ip = ipObject.getHostAddress(); //獲得IP地址字符串
                        System.out.println("小樣,抓到你了,鏈接我!!" + "IP:" + ip);
                        
                        //7,獲取Socket的輸入流
                        InputStream in = clientSocket.getInputStream();
                        //8,建立目的地的字節輸出流   D:\\upload\\192.168.74.58(1).jpg
                        BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\\upload\\"+ip+"("+System.currentTimeMillis()+").jpg"));
                        //9,把Socket輸入流中的數據,寫入目的地的字節輸出流中
                        byte[] buffer = new byte[1024];
                        int len = -1;
                        while((len = in.read(buffer)) != -1){
                            //寫入目的地的字節輸出流中
                            fileOut.write(buffer, 0, len);
                        }
                        
                        //-----------------反饋信息---------------------
                        //10,獲取Socket的輸出流, 做用:寫反饋信息給客戶端
                        OutputStream out = clientSocket.getOutputStream();
                        //11,寫反饋信息給客戶端
                        out.write("圖片上傳成功".getBytes());
                        
                        out.close();
                        fileOut.close();
                        in.close();
                        clientSocket.close();
                    } catch(IOException e){
                        e.printStackTrace();
                    }
                };
            }.start();
        }

        //serverSocket.close();
    }
}
複製代碼

3.總結

  • 建立線程的方式

  方式1,繼承Thread線程類

  步驟

    1, 自定義類繼承Thread類

    2, 在自定義類中重寫Thread類的run方法

    3, 建立自定義類對象(線程對象)

    4, 調用start方法,啓動線程,經過JVM,調用線程中的run方法

   方式2,實現Runnable接口

   步驟

    1, 建立線程任務類 實現Runnable接口

    2, 在線程任務類中 重寫接口中的run方法

    3, 建立線程任務類對象

    4, 建立線程對象,把線程任務類對象做爲Thread類構造方法的參數使用

    5, 調用start方法,啓動線程,經過JVM,調用線程任務類中的run方法

  • 同步鎖

  多個線程想保證線程安全,必需要使用同一個鎖對象

A.同步代碼塊

synchronized (鎖對象){
     可能產生線程安全問題的代碼
 }
複製代碼

同步代碼塊的鎖對象能夠是任意的對象

  

B.同步方法

public synchronized void method()
               可能產生線程安全問題的代碼
 }
複製代碼

同步方法中的鎖對象是 this

C.靜態同步方法

public synchronized static void method()
               可能產生線程安全問題的代碼
 }
複製代碼

靜態同步方法中的鎖對象是 類名.class

參考:

java編程思想

黑馬教學視頻

www.cnblogs.com/huangzhe151…

blog.csdn.net/qq_38545713…

www.cnblogs.com/dz-boss/p/1… (我的博客)

相關文章
相關標籤/搜索