02 如何建立線程 線程併發與synchornized

全部程序運行結果 請自行得出java

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

步驟:面試

1,定義一個類繼承Thread類。數組

2,覆蓋Thread類中的run方法。安全

3,直接建立Thread的子類對象建立線程。多線程

4,調用start方法開啓線程並調用線程的任務run方法執行。併發

 

 1 /*
 2  * 需求:咱們要實現多線程的程序。
 3  * 如何實現呢?
 4  *     因爲線程是依賴進程而存在的,因此咱們應該先建立一個進程出來。
 5  *     而進程是由系統建立的,因此咱們應該去調用系統功能建立一個進程。
 6  *     Java是不能直接調用系統功能的,因此,咱們沒有辦法直接實現多線程程序。
 7  *     可是呢?Java能夠去調用C/C++寫好的程序來實現多線程程序。
 8  *     由C/C++去調用系統功能建立進程,而後由Java去調用這樣的東西,
 9  *     而後提供一些類供咱們使用。咱們就能夠實現多線程程序了。
10 
11  * 那麼Java提供的類是什麼呢?
12  *         Thread
13  *         經過查看API,咱們知道了有2中方式實現多線程程序。
14  
15  * 方式1:繼承Thread類。
16  * 步驟
17  *     A:自定義類MyThread繼承Thread類。
18  *     B:MyThread類裏面重寫run()?
19  *     C:建立對象 new
20  *     D:啓動線程 start
21  */
22 
23 
24 /*
25  * 該類要重寫run()方法,爲何呢?
26  * 不是類中的全部代碼都須要被線程執行的。
27  * 而這個時候,爲了區分哪些代碼可以被線程執行,java提供了Thread類中的run()用來包含那些被線程執行的代碼。
28  */
29 
30 public class MyThread extends Thread {
31     @Override
32     public void run() {
33         // 本身寫代碼
34         // 通常來講,被線程執行的代碼確定是比較耗時的。因此咱們用循環
35         for (int x = 0; x < 200; x++) {
36             System.out.println(x);
37         }
38     }
39 }

若執行如下代碼:jvm

MyThread my = new MyThread();
my.run();
my.run();

觀察結果:程序是順序執行的,並無交替執行的現象的,爲何?ide

答:調用run()方法是單線程的。函數

      由於run()方法直接調用其實就至關於普通的方法調用,因此你看到的是單線程的效果this

要想看到多線程的效果,就必須說說另外一個方法:start()

MyThread my = new MyThread();

my.start();

 

面試題:run()和start()的區別?

run():僅僅是封裝被線程執行的代碼,直接調用是普通方法

start():首先啓動了線程,而後再由jvm去調用該線程的run()方法

 

因爲只start()了一次,觀察不到想要的結果,那麼就會想出以下的代碼:

MyThread my = new MyThread();

my.start();

my.start();

運行後會發現拋出錯誤: IllegalThreadStateException:非法的線程狀態異常

爲何呢?

由於start()2次至關因而my線程被調用了兩次,而不是兩個線程啓動。

一個線程只能調用一次

要想觀察到多線程的效果,正確的代碼是:

1 MyThread my1 = new MyThread();
2 MyThread my2 = new MyThread();
3 my1.start();
4 my2.start();

運行後就可以看見效果了。

 

1.1線程名字

經過運行結果咱們能夠發現:咱們並不知道誰是誰? 即哪一個線程在執行

如何獲取線程的名稱呢?  getName()方法

 1 public class MyThread extends Thread {
 2     public void run() {
 3        for (int x = 0; x < 100; x++) {
 4            System.out.println(getName() + ":" + x);
 5        }
 6     }
 7 }
 8 
 9 MyThread my1 = new MyThread();
10 MyThread my2 = new MyThread();
11 my1.start();
12 my2.start();

發現:能夠經過Thread的getName獲取線程的名稱 Thread-編號(從0開始)

名稱爲何是:Thread-? 編號

下面看一下Thread的代碼:

 1 class MyThread extends Thread {
 2     public MyThread() {
 3         super();
 4     }
 5 }
 6 
 7 class Thread {
 8     private char name[];
 9 
10     public Thread() {
11         init(null, null, "Thread-" + nextThreadNum(), 0);
12     }
13     
14     private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
15         init(g, target, name, stackSize, null);
16     }
17     
18      private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
19         //大部分代碼被省略了
20         this.name = name.toCharArray();
21     }
22     
23     public final void setName(String name) {
24         this.name = name.toCharArray();
25     }
26     
27     private static int threadInitNumber; //0,1,2
28     private static synchronized int nextThreadNum() {
29         return threadInitNumber++; //return 0,1
30     }
31     
32     public final String getName() {
33         return String.valueOf(name);
34     }
35 }

由原碼可知:Thread經過一個字符數組name表示線程名稱,用threadInitNumber變量來記錄線程的編號

                每當一個線程調用時,threadInitNumber++以達到計數的目的,而後 "Thread-" + threadInitNumber轉成字符數組給name

                調用getName()是將name轉換爲string,這就是咱們看到的效果。

但默認的方法對咱們來講每每沒有意義。但咱們能夠經過setName(String name)來設置咱們想要的名字

 1 咱們還能夠經過有參數的構造方法來命名線程
 2 public class MyThread extends Thread {
 3     public MyThread() {
 4     }
 5     
 6     public MyThread(String name){
 7         super(name);
 8     }
 9 
10     @Override
11     public void run() {
12         for (int x = 0; x < 100; x++) {
13             System.out.println(getName() + ":" + x);
14         }
15     }
16 }
17 //帶參構造方法給線程起名字
18 MyThread my1 = new MyThread("林青霞");
19 MyThread my2 = new MyThread("劉意");
20 my1.start();
21 my2.start();

 

主線程的名字就是main 咱們如何獲取呢?

遇到這種狀況,Thread類提供了一個很好玩的方法: public static Thread currentThread():返回當前正在執行的線程對象

在main方法中使用下面語句就能夠獲得多線程的名稱 System.out.println(Thread.currentThread().getName());

 

1.2線程調度優先級

 

假如咱們的計算機只有一個 CPU,那麼 CPU 在某一個時刻只能執行一條指令,

線程只有獲得 CPU時間片,也就是使用權,才能夠執行指令。

那麼Java是如何對線程進行調用的呢?

 

線程有兩種調度模型:

  分時調度模型     全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間片

  搶佔式調度模型   優先讓優先級高的線程使用 CPU,若是線程的優先級相同,那麼會隨機選擇一個,優先級高的線程獲取的 CPU 時間片相對多一些。

 

 Java使用的是搶佔式調度模型:

static int MAX_PRIORITY 線程能夠具備的最高優先級    10

static int MIN_PRIORITY  線程能夠具備的最低優先級   1

static int NORM_PRIORITY 分配給線程的默認優先級     5

 

public final void setPriority(int newPriority)更改線程的優先級

public final int getPriority()返回線程的優先級

 

 * 注意:

線程默認優先級是NORM_PRIORITY = 5

線程優先級的範圍是:1-10

線程優先級高僅僅表示線程獲取的 CPU時間片的概率高(並不是高優先級的線程必定每次都優先),

可是要在次數比較多,或者屢次運行的時候才能看到比較好的效果。

 1 public class ThreadPriority extends Thread {
 2 
 3     public void run() {
 4 
 5        for (int x = 0; x < 100; x++) {
 6            System.out.println(getName() + ":" + x);
 7        }
 8     }
 9 }
10 
11 ThreadPriority tp1 = new ThreadPriority();
12 ThreadPriority tp2 = new ThreadPriority();
13 ThreadPriority tp3 = new ThreadPriority();
14 
15  
16 tp1.setName("東方不敗");
17 tp2.setName("嶽不羣");
18 tp3.setName("林平之");
19 
20  
21 // 獲取默認優先級
22 System.out.println(tp1.getPriority());
23 System.out.println(tp2.getPriority());
24 System.out.println(tp3.getPriority());
25 
28 //設置線程優先級
29 tp1.setPriority(10);
30 tp2.setPriority(1);
31 
34 tp1.start();
35 tp2.start();
36 tp3.start();
37 

 

若是設置線程優先級: tp1.setPriority(100000);

報錯:

IllegalArgumentException:非法參數異常。

拋出的異常代表向方法傳遞了一個不合法或不正確的參數。

 

1.3 sleep

 線程休眠  public static void sleep(long millis)

  Thread.sleep(1000); //1秒

  在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),

  此操做受到系統計時器和調度程序精度和準確性的影響。

  該線程不丟失任何監視器的所屬權時間到後該線程繼續執行 即不會執行的權利 時間到後繼續往下走

咱們能夠經過如下代碼感覺:

 1 public class ThreadSleep extends Thread {
 2     @Override
 3     public void run() {
 4         for (int x = 0; x < 100; x++) {
 5             System.out.println(getName() + ":" + x + ",日期:" + new Date());
 6             try {
 7                 Thread.sleep(1000);
 8             } catch (InterruptedException e) {
 9                 e.printStackTrace();
10             }
11         }
12     }
13 }
14 public class ThreadSleepDemo {
15     public static void main(String[] args) {
16         ThreadSleep ts1 = new ThreadSleep();
17         ThreadSleep ts2 = new ThreadSleep();
18         ThreadSleep ts3 = new ThreadSleep();
19 
20         ts1.setName("林青霞");
21         ts2.setName("林志玲");
22         ts3.setName("林志穎");
23 
24         ts1.start();
25         ts2.start();
26         ts3.start();
27     }
28 }

 

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

1,定義類實現Runnable接口。

2,覆蓋接口中的run方法,將線程的任務代碼封裝到run方法中。

3,經過Thread類建立線程對象,並將Runnable接口的子類對象做爲Thread類的構造函數的參數進行傳遞。

    爲何?

       爲線程的任務都封裝在Runnable接口子類對象的run方法中。

      因此要在線程對象建立時就必須明確要運行的任務。

4,調用線程對象的start方法開啓線程。

 

實現Runnable接口的好處:

1,將線程的任務從線程的子類中分離出來,進行了單獨的封裝。按照面向對象的思想將任務的封裝成對象。

2,避免了java單繼承的侷限性。

因此,建立線程的第二種方式較爲經常使用。

實例

 1 /*
 2  * 方式2:實現Runnable接口
 3  * 步驟:
 4  *         A:自定義類MyRunnable實現Runnable接口
 5  *         B:重寫run()方法
 6  *         C:建立MyRunnable類的對象
 7  *         D:建立Thread類的對象,並把C步驟的對象做爲構造參數傳遞
 8  */
 9 
10 public class MyRunnable implements Runnable {
11 
12     @Override
13     public void run() {
14         for (int x = 0; x < 100; x++) {
15             // 因爲實現接口的方式就不能直接使用Thread類的方法了,可是能夠間接的使用
16             System.out.println(Thread.currentThread().getName() + ":" + x);
17         }
18     }
19 
20 }
21 
22 使用:
23 MyRunnable my = new MyRunnable();
24 // 建立Thread類的對象,並把C步驟的對象做爲構造參數傳遞 Thread(Runnable target)
25 Thread t1 = new Thread(my);
26 Thread t2 = new Thread(my);
27 t1.setName("林青霞");
28 t2.setName("劉意");
29 t1.start();
30 t2.start();
31 
32 MyRunnable my = new MyRunnable();
33 // Thread(Runnable target, String name)
34 Thread t1 = new Thread(my, "林青霞");
35 Thread t2 = new Thread(my, "劉意");
36 t1.start();
37 t2.start();

2.1比較

2.2 實現Runnable的最大好處就是能夠實現共享同一資源

案例2:經典的買票問題

某電影院目前正在上映賀歲大片(紅高粱,少林寺傳奇藏經閣),

共有100張票,而它有3個售票窗口售票,請設計一個程序模擬該電影院售票。

 

先用Thread實現

public class SellTicket extends Thread {

    // 定義100張票
    // private int tickets = 100;
    // 爲了讓多個線程對象共享這100張票,咱們其實應該用靜態修飾
    private static int tickets = 100;

    public void run() {

       // 定義100張票
       // 每一個線程進來都會走這裏,這樣的話,每一個線程對象至關於買的是本身的那100張票,這不合理,因此應該定義到外面
       // int tickets = 100;
       // 是爲了模擬一直有票
       while (true) {
           if (tickets > 0) {
              System.out.println(getName() + "正在出售第" + (tickets--) + "張票");
           }
       }
    }
}

public class SellTicketDemo {

    public static void main(String[] args) {

       // 建立三個線程對象
       SellTicket st1 = new SellTicket();
       SellTicket st2 = new SellTicket();
       SellTicket st3 = new SellTicket();

       // 給線程對象起名字
       st1.setName("窗口1");
       st2.setName("窗口2");
       st3.setName("窗口3");

       // 啓動線程
       st1.start();
       st2.start();
       st3.start();
    }
}

 

如今用Runable接口:

 1 public class SellTicket implements Runnable {
 2     // 定義100張票
 3     private int tickets = 100;
 4     public void run() {
 5        while (true) {
 6            if (tickets > 0) {
 7 
 8               try{Thread.sleep(100)}catch(Exception e){}
 9               
10               System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "張票");
11            }
12        }
13     }
14 }

new Thread(new SellTicket()).start();
new Thread(new SellTicket()).start();
new Thread(new SellTicket()).start();

 

咱們每次賣票都延遲100毫秒,此時你就會出現問題

緣由就是由於多個線程併發致使了共享數據的安全問題

 

如何解決線程安全問題呢?

 要想解決問題,就要知道哪些緣由會致使出問題:(並且這些緣由也是之後咱們判斷一個程序是否會有線程安全問題的標準)

 A:是不是多線程環境

 B:是否有共享數據

 C:是否有多條語句操做共享數據

咱們來回想一下咱們的程序有沒有上面的問題呢?

A:是不是多線程環境  是

B:是否有共享數據   是

C:是否有多條語句操做共享數據   是

 

因而可知咱們的程序出現問題是正常的,由於它知足出問題的條件。

接下來纔是咱們要想一想如何解決問題呢?

A和B的問題咱們改變不了,咱們只能想辦法去把C改變一下。

 

Solution:

 基本上全部的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問

 一般來講,是在訪問臨界資源前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其餘線程繼續訪問。

   解決思就是將多條操做共享數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其餘線程時不能夠參與運算的。必需要當前線程把這些代碼都執行完畢後,其餘線程才能夠參與運算。 

 

Java提供的解決方案

思想:

把多條語句操做共享數據的代碼給包成一個總體,讓某個線程在執行的時候,別人不能來執行。

問題是咱們不知道怎麼包啊?Java給咱們提供了:同步機制。

 

解決思就是將多條操做共享數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,

其餘線程時不能夠參與運算的。必需要當前線程把這些代碼都執行完畢後,其餘線程才能夠參與運算。

在java中,用同步代碼塊就能夠解決這個問題。

 

同步代碼塊的格式:

synchronized(對象)

{

    須要被同步的代碼

}

 

A:對象是什麼呢?

    咱們能夠隨便建立一個對象試試。(對象其實能夠是任意類型的,但必須是同一個對象)

       synchronized(new Object()){}  No

       synchronized(object){}         Yes  可使用 class文件

 

B:須要同步的代碼是哪些呢?

  把多條語句操做共享數據的代碼的部分給包起來

 

注意:

同步能夠解決安全問題的根本緣由就在那個對象上。該對象如同鎖的功能。

多個線程必須是同一把鎖。

 

同步的好處:解決了線程的安全問題。

同步的弊端:相對下降了效率,由於同步外的線程的都會判斷同步鎖。

同步的前提:同步中必須有多個線程並使用同一個鎖

 

l  synchronized 代碼塊

 synchronized(...){
   ..... } 
賣票問題的解決:
 1 public class ThreadDemo03 {
 2     public static void main(String[] args) 
 3     {
 4         Ticket t = new Ticket();//建立一個線程任務對象  此時只有一個Ticket對象  且只能有一個
 5 
 6         Thread t1 = new Thread(t);
 7         Thread t2 = new Thread(t);
 8         Thread t3 = new Thread(t);
 9         Thread t4 = new Thread(t);
10 
11         t1.start();
12         t2.start();
13         t3.start();
14         t4.start();
15     }
16 }
17 
18 class Ticket implements Runnable{
19     private  int num = 100;    //共享資源 必須放在這裏 而不能在run()中
20     Object obj = new Object();//對象鎖只能有一個
21 
22     public void run()
23     {
24         while(true)
25         {
26             synchronized(obj)
27             {
28                 if(num>0)
29                 {
30                     try{Thread.sleep(1000);}catch (InterruptedException e){}
31                     System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
32                 }
33             }
34         }
35     }
36 }

l  synchronized 方法:

public synchronized type function (... ...); 
 1 private static synchronized void sellTicket() {
 2         if (tickets > 0) {
 3         try {
 4                 Thread.sleep(100);
 5         } catch (InterruptedException e) {
 6                 e.printStackTrace();
 7         }
 8         System.out.println(Thread.currentThread().getName()
 9                     + "正在出售第" + (tickets--) + "張票 ");
10         }
11 }

 

l  當一個線程進入一個對象的一個synchronized方法後,其它線程是否可進入此對象的其它方法?

   對象的synchronized方法不能進入了,但它的其餘非synchronized方法仍是能夠訪問的

 1 public class TT implements Runnable {
 2 
 3      int b = 100;
 4 
 5      public synchronized void m1(){
 6             b = 10000;
 7             try {
 8                 Thread. sleep(5000);
 9                 System. out.println( "m1----b="+ b);
10            } catch (InterruptedException e) {
11                 e.printStackTrace();
12            }
13      }
14 
15      public void m2(){
16            System. out.println( "m2====="+ b);
17      }
18 
19      @Override
20      public void run() {
21            m1();
22      }
23 
24      public static void main(String[] args) {
25            TT tt = new TT();
26            Thread t = new Thread(tt);
27            t.start();
28 
29             try {
30                 Thread. sleep(1000);
31            } catch (InterruptedException e) {
32                 e.printStackTrace();
33            }
34 
35            tt.m2();
36      }
37 
38 }

 

鎖對象是誰?

 * A:同步代碼塊的鎖對象是誰呢?

 *     任意對象。

 

 * B:同步方法的格式及鎖對象問題?

 *     把同步關鍵字加在方法上。

 *     同步方法是誰呢?

 *         this

 

 * C:靜態方法及鎖對象問題?

 *     靜態方法的鎖對象是誰呢?

 *     類的字節碼文件對象。(反射會講)

 

 

線程安全的類

StringBuffer sb = new StringBuffer();

Vector<String> v = new Vector<String>();

Hashtable<String, String> h = new Hashtable<String, String>();

Vector是線程安全的時候纔去考慮使用的,可是即便要安全,我也不用你  

那麼到底用誰呢?public static <T> List<T> synchronizedList(List<T> list)

List<String> list1 = new ArrayList<String>();// 線程不安全

List<String> list2 = Collections.synchronizedList(new ArrayList<String>()); // 線程安全

 

synchornized

synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。

若是一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其餘線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這裏獲取鎖的線程釋放鎖只會有兩種狀況:

  1)獲取鎖的線程執行完了該代碼塊,而後線程釋放對鎖的佔有;

  2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。

 

原理示意圖

jdk1.5還提供了lock鎖 後面再說

相關文章
相關標籤/搜索