Java 線程學習

Java 線程相關

  1. 如何建立線程(兩種方式,區別,使用場景)
  2. 線程狀態調度
  3. 多線程數據共享(會有什麼問題,如何實現共享,多線程操做同一個變量會有什麼問題,若是不但願有問題怎麼作)
  4. 數據傳遞
  5. 線程池相關(如何建立線程池,要注意什麼(初始化線程內部變量),幾種經常使用的使用方式)

1. 線程建立

一般建立線程有兩種方式,一個是繼承 Thread, 一個是實現 Runnable; 下面則分別實現以作演示,而後說一下這兩種的區別,應該如何選擇html

建立線程

建立線程和使用的一個小case以下, 注意的是線程啓動是調用start方法, 而不是 run 方法; 其次實現Runnable 接口的類,啓動依然是放在一個Thread 對象中java

public class ThreadCreate {


    /**
     * 經過繼承  Thread 方式來建立一個新的線程
     */
    public static class ThreadExtend extends Thread {

        @Test
        public void run() {
            System.out.println("new extend thread");
        }
    }


    /**
     * 經過實現 Runnable 方式來建立一個線程
     */
    public static class RunnableImplement implements Runnable {

        @Override
        public void run() {
            System.out.println("new runnable thread");
        }
    }

    @Test
    public void testCreate() {
        new ThreadExtend().start();

        new Thread(new RunnableImplement()).start();

        System.out.println("main!");
    }

}

兩種方式對比

爲何會有兩種方式呢?這兩種的區別何在?數組

  1. 實現是能夠有多個的,可是繼承只能有一個父類
  2. 查看 Runnable 的使用方法,最終是放在一個 Thread裏面去執行的,因此在多個相同的程序代碼處理一個資源時,這個仍是有優點的;可是查看 Thread實際上就是 Runnable的實現,一樣能夠將一個自定義的Thread對象,建立多個 Thread對象來調用

經過上面的描述能夠知道一點,若是你但願數據多線程內共享,不妨考慮實現 Runnable 接口(固然繼承Thread也是ok的);若是但願隔離,則不妨考慮繼承Thread (實際上使用 Runnable接口的實現也是ok的,多建立幾個實現類接口對象而已,每一個對象放在一個新的Thread中執行)多線程

按照我的的理解,網上說的實現Runnable 方便資源共享,更多的是傾向於代碼的共享,一般是一個Runnable對象,放在多個 Thread實例中執行;而繼承 Thead類,從出發點來看,繼承的通常是做爲一個獨立線程來執行使用,若是你真要像下面這麼作,也不會報錯,也能正常運行,只是有點違反設計理念而已併發

MyThread extreds Thread {...};
MyThread mythread = new MyThread();
new Thread(mythread).start();

case 舉例

舉一個例子,車站賣票,假設如今有三個窗口,總共只有30張車票,賣完就不賣了,怎麼實現?若是每一個窗口有10張車票,各個窗口把本身的賣完了就不賣了,怎麼實現?dom

第一個case,符合數據共享的一種場景,那麼咱們的實現能夠以下:ide

public static class TotalSaleTick implements Runnable {
   private int total = 30;

   @Override
   public void run() {
       while (true) {
           if (total > 0) {
               System.out.println(Thread.currentThread().getName() + "售出一張,剩餘:" + --total);
           } else {
               break;
           }
       }
   }
}


@Test
public void testTotalSale() {
   TotalSaleTick totalSaleTick = new TotalSaleTick();
   Thread thread1 = new Thread(totalSaleTick, "窗口1");
   Thread thread2 = new Thread(totalSaleTick, "窗口2");
   Thread thread3 = new Thread(totalSaleTick, "窗口3");

   thread1.start();
   thread2.start();
   thread3.start();
   System.out.println("master over!");
}

輸出以下, 基本上每次跑的輸出結果都不同, 能夠看出的一點是三個窗口售出的票數不一樣,一個問題,上面這種狀況,可能形成超賣麼?學習

窗口1售出一張,剩餘:29
master over!
窗口2售出一張,剩餘:28
窗口2售出一張,剩餘:25
窗口2售出一張,剩餘:24
窗口1售出一張,剩餘:27
窗口3售出一張,剩餘:26
窗口1售出一張,剩餘:22
窗口1售出一張,剩餘:20
窗口1售出一張,剩餘:19
窗口1售出一張,剩餘:18
窗口1售出一張,剩餘:17
窗口1售出一張,剩餘:16
窗口1售出一張,剩餘:15
窗口1售出一張,剩餘:14
窗口1售出一張,剩餘:13
窗口1售出一張,剩餘:12
窗口1售出一張,剩餘:11
窗口1售出一張,剩餘:10
窗口2售出一張,剩餘:23
窗口2售出一張,剩餘:8
窗口2售出一張,剩餘:7
窗口2售出一張,剩餘:6
窗口2售出一張,剩餘:5
窗口2售出一張,剩餘:4
窗口1售出一張,剩餘:9
窗口3售出一張,剩餘:21
窗口3售出一張,剩餘:1
窗口3售出一張,剩餘:0
窗口1售出一張,剩餘:2
窗口2售出一張,剩餘:3

第二個case,則顯然更傾向於繼承 Thread 來實現了測試

public static class SplitSaleTick extends Thread {
   private int total = 10;

   public SplitSaleTick(String name) {
       super(name);
   }

   @Override
   public void run() {
       while (true) {
           if (total > 0) {
               System.out.println(Thread.currentThread().getName() + "售出一張,剩餘:" + --total);
           } else {
               break;
           }
       }
   }
}


@Test
public void testSplitSaleTick() {
   SplitSaleTick splitSaleTick1 = new SplitSaleTick("窗口1");
   SplitSaleTick splitSaleTick2 = new SplitSaleTick("窗口2");
   SplitSaleTick splitSaleTick3 = new SplitSaleTick("窗口3");

   splitSaleTick1.start();
   splitSaleTick2.start();
   splitSaleTick3.start();
   System.out.println("master over");
}


/**
* 繼承 Thread 也能夠實現共享, 只不過比較噁心而已
*/
@Test
public void testSplitSaleTick2() {
   SplitSaleTick splitSaleTick1 = new SplitSaleTick("saleTick");

   Thread thread1 = new Thread(splitSaleTick1, "窗口1");
   Thread thread2 = new Thread(splitSaleTick1, "窗口2");
   Thread thread3 = new Thread(splitSaleTick1, "窗口3");

   thread1.start();
   thread2.start();
   thread3.start();
}

輸出接過以下, 三個窗口能夠併發賣,且每一個窗口賣10張,賣完即止this

窗口1售出一張,剩餘:9
窗口2售出一張,剩餘:9
窗口2售出一張,剩餘:8
窗口1售出一張,剩餘:8
窗口1售出一張,剩餘:7
窗口1售出一張,剩餘:6
窗口1售出一張,剩餘:5
窗口1售出一張,剩餘:4
窗口2售出一張,剩餘:7
窗口1售出一張,剩餘:3
窗口1售出一張,剩餘:2
窗口1售出一張,剩餘:1
窗口1售出一張,剩餘:0
窗口3售出一張,剩餘:9
窗口3售出一張,剩餘:8
窗口3售出一張,剩餘:7
窗口3售出一張,剩餘:6
窗口3售出一張,剩餘:5
窗口3售出一張,剩餘:4
窗口3售出一張,剩餘:3
窗口3售出一張,剩餘:2
窗口3售出一張,剩餘:1
窗口3售出一張,剩餘:0
master over
窗口2售出一張,剩餘:6
窗口2售出一張,剩餘:5
窗口2售出一張,剩餘:4
窗口2售出一張,剩餘:3
窗口2售出一張,剩餘:2
窗口2售出一張,剩餘:1
窗口2售出一張,剩餘:0


---- test2 輸出 ----
窗口1售出一張,剩餘:9
窗口1售出一張,剩餘:6
窗口1售出一張,剩餘:5
窗口1售出一張,剩餘:4
窗口1售出一張,剩餘:3
窗口1售出一張,剩餘:2
窗口3售出一張,剩餘:7
窗口2售出一張,剩餘:8
窗口3售出一張,剩餘:0
窗口1售出一張,剩餘:1

2. 線程狀態(線程生命週期)

線程建立以後,即調用了start方法以後,線程是否開始運行了?這個運行過程是否會暫停呢?若是須要暫停應該怎麼辦;若是一個線程依賴另外一個線程的計算結果,又該如何處理?

  • 建立:新建一個線程對象,如Thread thd=new Thread()
  • 就緒:建立了線程對象後,調用了線程的start()方法(此時線程知識進入了線程隊列,等待獲取CPU服務 ,具有了運行的條件,但並不必定已經開始運行了)
  • 運行:處於就緒狀態的線程,一旦獲取了CPU資源,便進入到運行狀態,開始執行run()方法裏面的邏輯
  • 終止:線程的run()方法執行完畢,或者線程調用了stop()方法,線程便進入終止狀態
  • 阻塞:一個正在執行的線程在某些狀況系,因爲某種緣由而暫時讓出了CPU資源,暫停了本身的執行,便進入了阻塞狀態,如調用了sleep()方法
  • 線程讓步: join 等待其餘線程終止。在當前線程中調用另外一個線程的join()方法,則當前線程轉入阻塞狀態,直到另外一個進程運行結束,當前線程再由阻塞轉爲就緒狀態

3. 方法說明

一個Thread實例有一些經常使用的方法如: start, sleep, run, yield, join, wait 等, 這些方法是幹嗎用的,什麼場景下使用,使用時須要注意些什麼?

方法的執行,將對應線程狀態進行說明

run 方法

run 方法中爲具體的線程執行的代碼邏輯,通常而言,都不該該被直接進行調用

不管咱們採用哪一種方法建立線程,基本上都是要重寫run 方法,這個方法會在線程執行時調用

start 方法

執行該方法以後,線程進入就緒狀態,對使用者而言,但願線程執行就是調用的這個方法(注意調用以後不會當即執行)

這個方法的主要目的就是告訴系統,咱們的線程準備好了,cpu有空了趕忙來執行咱們的線程

sleep 方法

睡眠一段時間,這個過程當中不會釋放線程持有的鎖, 傳入int類型的參數,表示睡眠多少ms

讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留必定時間給其餘線程執行的機會

咱們最多見的一種使用方式是在主線程中直接調用 Thread.sleep(100) , 表示先等個100ms, 而後再繼續執行

wait 方法

wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還須要返還對象鎖);其餘線程能夠訪問

wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程

一般咱們執行wait方法是由於當前線程的執行,可能依賴到其餘線程,如登陸線程中,若發現用戶沒有註冊,則等待,等用戶註冊成功後繼續走登陸流程(咱們不考慮這個邏輯是否符合實際),

這裏就能夠在登陸線程中調用 wait方法, 在註冊線程中,在執行完畢以後,調用notify方法通知登陸線程,註冊完畢,而後繼續進行登陸後續action

yield 方法

暫停當前正在執行的線程對象,並執行其餘線程

yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中

這個方法的執行,有點像一個拿到麪包的人對另外幾我的說,我把麪包放在桌上,咱們重新開始搶,那麼下一個拿到麪包的仍是這些人中的某個(你們機會均等)

想象不出啥時候會這麼幹

join 方法

啓動線程後直接調用,即join()的做用是:「等待該線程終止」,這裏須要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行

從上面的描述也能夠很容易看出什麼場景須要調用這個方法,主線程和子線程誰先結束很差說,若是主線程提早結束了,致使整個應用都關了,這個時候子線程沒執行完,就呵呵了;其次就是子線程執行一系列計算,主線程會用到計算結果,那麼就能夠執行這個方法,保證子線程執行完畢後再使用計算結果

4. 數據共享

多線程間數據共享,當多線程公用一個Runnable對象時,這個對象中的成員變量便可以達到數據共享的目的;多線程採用不一樣的Runnable對象時,數據怎麼共享

公用 Runnable對象時

上面的售票例子中,其實就有這個場景,上面提出了一個問題,是否會出現超賣的狀況?

  1. 由於咱們知道 ++ 不是原子操做, 實際能夠拆分爲三步:

    • 內存到寄存器
    • 寄存器自增
    • 寫回內存

    假設num爲10時, 線程A和線程B都調用 ++num操做;對於內存到寄存器這一步,兩個線程都到了這一步,A自增將11寫回內存,B也進行自增將11寫會內存,這個時候就少+1了

  2. 讀一個long,double類型的共享變量時,也不是原子操做,在32位操做系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據,若是有兩個線程同時寫一個變量內存,一個進程寫低32位,而另外一個寫高32位,這樣將致使獲取的64位數據是失效的數據

在多線程中,共享數據的獲取or更新,請確保是原子操做;能夠考慮同步鎖(synchronized)修改共享變量,共享變量前添加volatile, 使用原子數據類型 AtomicInteger

修改上面的售票代碼以下

public static class TotalSaleTick implements Runnable {
        private int total = 30;

        @Override
        public void run() {
            while (true) {
                synchronized (this) {
                    if (total > 0) {
                        System.out.println(Thread.currentThread().getName() + "售出一張,剩餘:" + --total);
                    } else {
                        break;
                    }
                }
            }
        }
    }

一個小疑惑,在實際的測試中,即使是上面不加上同步塊,好像也沒有出問題,對於上面的操做可能運行不少遍都是正確的, 好像和咱們預期的不相符,有沒有多是由於總數太少,致使衝突的機率變小了?

private AtomicInteger count = new AtomicInteger(0);
private int sum = 3000;

public class MyThread extends Thread {
   public void run() {
       while (true) {
           if (sum > 0) {
               count.addAndGet(1);
               --sum;
           }else {
               break;
           }
       }
       System.out.println(Thread.currentThread().getName() + " over " + sum);
   }
}


@Test
public void testAdd() throws InterruptedException {
   MyThread myThread1 = new MyThread();
   MyThread myThread2 = new MyThread();

   myThread1.start();
   myThread2.start();

   myThread1.join();
   myThread2.join();

   System.out.println("num: " + sum + " count: " + count.get());
}

對上面的場景,多運行幾回,發現輸出結果果真是超賣了

Thread-1 over -1
Thread-0 over -1
num: -1 count: 3008

非公用的 Runnable 對象時

共享全局變量 + 共享局部變量兩種狀況,有點區別

上面的case就是一個共享全局變量的demo,上面出現了併發衝突,能夠以下解決, 針對類進行加鎖

public class ThreadShareTest {
    private AtomicInteger count = new AtomicInteger(0);
    private int sum = 3000;

    public class MyThread extends Thread {
        public void run() {
            while (true) {
                if (sum > 0) {
                    synchronized (ThreadShareTest.class) {
                        if (sum > 0) {
                            count.addAndGet(1);
                            --sum;
                        }
                    }
                }else {
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + " over " + sum);
        }
    }


    @Test
    public void testAdd() throws InterruptedException {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        myThread1.start();
        myThread2.start();

        myThread1.join();
        myThread2.join();

        System.out.println("num: " + sum + " count: " + count.get());
    }
}

共享局部變量,須要注意的是局部變量要求是final, 因此下面的int採用了數組的形式(基本類型沒法修改,引用類型能夠改其內部的值, 不能改引用)

@Test
public void testAdd2() throws InterruptedException {
   final int[] num = {3000};
   final AtomicInteger c = new AtomicInteger(0);


   Runnable runnable = new Runnable() {
       @Override
       public void run() {
           while (true) {
               if (num[0] > 0) {
                   c.addAndGet(1);
                   num[0]--;
               } else {
                   break;
               }
           }

           System.out.println(Thread.currentThread().getName() + " over " + num[0]);
       }
   };

   Thread thread1 = new Thread(runnable);
   Thread thread2 = new Thread(runnable);

   thread1.start();
   thread2.start();

   thread1.join();
   thread2.join();

   System.out.println("num: " + num[0] + " count: " + c.get());
}

多運行幾回,輸出以下,說明也存在併發的問題了, 修正方式一樣是加鎖

Thread-0 over -1
Thread-1 over -1
num: -1 count: 3001

修改後的run方法內部以下

while (true) {
     if (num[0] > 0) {
         synchronized (this) {
             if (num[0] > 0) {
                 c.addAndGet(1);
                 num[0]--;
             } else {
                 break;
             }
         }
     } else {
         break;
     }
 }

線程數據隔離

上面是數據在多線程中共享,很容易出現的就是併發問題;還有一個場景就是我但願不存在數據共享,線程操做的內部變量不影響其餘的線程; 最簡單的想法就是一個繼承了Thread的類,其內部類正常來說就是隔離的,只要你不把它當成 Runnable 接口的使用方式就行

使用 ThreadLocal 來保證變量在線程之間的隔離, 下面是一個簡單的演示,兩個線程都是在修改threadLocal中的值, 可是兩個線程的修改,對彼此而言是獨立的

public static class LocalT implements Runnable {
   ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

   @Override
   public void run() {
       int start = (int) (Math.random() * 100);
       for (int i =0 ; i < 100; i = i+2) {
           threadLocal.set(start + i);
           System.out.println(Thread.currentThread().getName() + " : " + get());
       }
   }

   public int get() {
       return threadLocal.get();
   }
}


@Test
public void testLocal() throws InterruptedException {
   LocalT local = new LocalT();

   Thread thread1 = new Thread(local);
   Thread thread2 = new Thread(local);

   thread1.start();
   thread2.start();

   thread1.join();
   thread2.join();
}

5. 數據傳遞

數據如何傳遞給線程,有如何把線程計算的結果拋出來

傳遞數據

比較容易想到的就是在建立對象時,傳入數據;或者調用線程對象的setXXX方法傳入數據, 當作正常的對來操做處理便可

須要注意的是,在線程的執行期間,你修改了其中的局部變量,會出現什麼狀況呢?

public static class ThreadData implements Runnable {
   private int num = 0;


   public void run() {
       while (num < 100) {
           System.out.println(Thread.currentThread().getName() + " now: " + num++);
       }

       System.out.println(Thread.currentThread().getName() + " num: " + num);
   }

   public void setNum(int num) {
       System.out.println(this.num + " now set to " + num);
       this.num = num;
   }
}


@Test
public void testThreadSetData() throws InterruptedException {
   ThreadData threadData = new ThreadData();

   Thread thread1 = new Thread(threadData);
   Thread thread2 = new Thread(threadData);

   thread1.start();
   thread2.start();

   threadData.setNum(200);

   thread1.join();
   thread1.join();
}

輸出以下, 將num設置爲200以後,並無如咱們預期的結束線程,依然在往下走, 這裏就至關因而有一個你修改了這個數據,是否會立馬就生效呢?特別是對其餘的線程而言

...
Thread-1 now: 24
Thread-1 now: 25
Thread-0 now: 14
26 now set to 200
Thread-0 now: 27
Thread-0 now: 28
Thread-1 now: 26
Thread-0 now: 29
Thread-1 now: 30
....

輸出結果

線程執行了一個任務以後,輸出的結果能夠怎麼處理

一個實例,一個線程實現累加的過程,我如今但願實現1 加到 100, 開四個線程,怎麼作?

下面是一個實現,不知道有沒有什麼問題

public static class CalculateThread extends Thread {

   private int start;
   private int end;

   private int ans;

   public CalculateThread(int start, int end) {
       this.start = start;
       this.end = end;
   }

   public void run() {
       for (int i = start; i <= end; i++) {
           ans += i;
       }
   }

   public int getAns() {
       return ans;
   }
}


@Test
public void testCalculate() throws InterruptedException {
   CalculateThread c1 = new CalculateThread(1, 25);
   CalculateThread c2 = new CalculateThread(26, 50);
   CalculateThread c3 = new CalculateThread(51, 75);
   CalculateThread c4 = new CalculateThread(76, 100);

   c1.start();
   c2.start();
   c3.start();
   c4.start();


   c1.join();
   c2.join();
   c3.join();
   c4.join();

   System.out.println("ans1: " + c1.getAns() + " ans2: " + c2.getAns() + " ans3: " + c3.getAns() + " ans4: " + c4.getAns());
   int ans = c1.getAns() + c2.getAns() + c3.getAns() + c4.getAns();
   System.out.println("ans : " + ans);
}

6. 線程池

多線程技術主要解決處理器單元內多個線程執行的問題,它能夠顯著減小處理器單元的閒置時間,增長處理器單元的吞吐能力 線程的頻繁建立和銷燬可能浪費大量的時間,線程池就是爲了解決這個問題而產生

參考

相關文章
相關標籤/搜索