Java-Se-多線程

進程 & 線程

進程:是系統進行資源分配的和調度的一個獨立單位。舉例:將公司的一個部門看做一個進程,公司分配項目獎金的時候就是以部門爲基本單位分配1萬塊錢,部門下面的十幾我的就分這1萬塊錢,這時就能夠將部門中的每一個人看做是一個線程。這就是進程與線程之間的關係。java

線程:線程是進程的執行單元,一個進程能夠有多個線程。編程

進程間能夠併發,線程間也能夠併發。數組

什麼是併發,什麼是並行?????緩存

併發(concurrency)和並行(parallellism)是: 解釋一:並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生。 解釋二:並行是在不一樣實體上的多個事件,併發是在同一實體上的多個事件。 解釋三:在一臺處理器上「同時」處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分佈式集羣 因此併發編程的目標是充分的利用處理器的每個核,以達到最高的處理性能。-- 併發編程網安全

所以,多線程編程解決的是併發問題,並行的問題能夠經過分佈式部署來解決。???多線程

建立線程的3種方式

繼承Thread類

步驟:
一、 定義Thread類的子類,並重寫run()方法。
二、 建立Thread子類的實現
三、 調用線程對象的start()方法啓動線程併發

代碼片斷:dom

// 經過繼承Thread類來建立線程類
public class FirstThread extends Thread
{
    // 重寫run方法,run方法的方法體就是線程執行體
    public void run()
    {

    }
    public static void main(String[] args)
    {
        // 建立、並啓動第一條線程
        new FirstThread().start();
        // 建立、並啓動第二條線程
        new FirstThread().start();
    }
}

實現Runnable接口

步驟:
一、定義Runnable接口的實現類,並重寫該接口的run()方法
二、建立該實現類的實例,並以此實例做爲Thread的target來建立Thread對象
三、調用該線程對象的start()方法來啓動線程分佈式

代碼片斷:ide

// 經過實現Runnable接口來建立線程類
public class SecondThread implements Runnable
{
    // run方法一樣是線程執行體
    public void run()
    {
       
    }

    public static void main(String[] args)
    {
       SecondThread st = new SecondThread();     // ①
        // 經過new Thread(target , name)方法建立新線程
        new Thread(st , "新線程1").start();
        new Thread(st , "新線程2").start();
    }
}

Callable和Future

步驟:
一、建立Callable接口實現類,並實現call()方法
二、使用FutureTask類來包裝Callable對象
三、使用FutureTask對象做爲Thread對象的target建立並啓動線程
四、經過FutureTask對象的get()方法獲取線程的返回值

代碼片斷:

@Test
    public void test() throws Exception {
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {

            public Integer call() throws Exception {
                return 1;
            }
        });

        new Thread(task, "新的線程1").start();

        Integer i = task.get();
        System.out.println("i:" + i);
    }

線程生命週期

線程的生命週期有:新建、就緒、運行、阻塞、死亡。

線程對象調用start()方法後就處於就緒狀態。

線程生命週期的狀態轉換圖以下: 線程生命週期狀態轉換圖

總結:

  • wait(),進入阻塞狀態,釋放同步監視器
  • sleep(),進入阻塞狀態,給其它進程(無論線程優先級)執行的機會,不會釋放同步監視器
  • join(),調用線程進入阻塞狀態,直到加入的線程執行完成爲止
  • yield(),不進入阻塞狀態,給其它線程(只給優先級高)執行的機會,不會釋放同步監視器

控制線程

join

join:讓一個線程等待另外一個線程完成。

setDaemon(true)

設置爲後臺線程,調用線程實例自己的setDaemon()方法:

MyThread myThread = new MyThread();
        myThread.setDaemon(true);

sleep

線程睡眠,並進入阻塞狀態。

調用線程Thread類的靜態sleep()方法,而不是某個線程的實例方法:

Thread.sleep(1000);

yield

線程暫停,但不進入阻塞狀態。

調用線程Thread類的靜態yield()方法,而不是某個線程的實例方法:

Thread.yield();

sleep()與yield()區別

  • sleep()與yield()都會讓當前線程暫停,sleep()會給其它線程執行的機會,不理會其它線程的優先級;但yield()只給優先級相同或更高的線程執行的機會。
  • sleep()會讓線程進入阻塞狀態,yield()則不會進入阻塞狀態。

線程優先級

priority,1-10,數值越大優先級越高:

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5
myThread.setPriority(Thread.MAX_PRIORITY);

注:servlet中的 load-on-startup 參數,值越大優先級越低

線程同步

傳統線程同步

同步方法

同步一個易發生線程安全的方法,這裏的鎖就是this,即當前類對象

// 提供一個線程安全draw()方法來完成取錢操做
    public synchronized void draw(double drawAmount)
    {
        
    }

同步代碼塊

同步一個易發生線程安全的字段,這裏的鎖就是此可能發生線程安全的字段

public class DrawThread extends Thread
{
    // 模擬用戶帳戶
    private Account account;

    // 當多條線程修改同一個共享數據時,將涉及數據安全問題。
    public void run()
    {
        // 使用account做爲同步監視器,任何線程進入下面同步代碼塊以前,
        // 必須先得到對account帳戶的鎖定——其餘線程沒法得到鎖,也就沒法修改它
        // 這種作法符合:「加鎖 → 修改 → 釋放鎖」的邏輯
        synchronized (account)
        {
           
        }
        // 同步代碼塊結束,該線程釋放同步鎖
    }
}

什麼時候釋放同步監視器

  • wait()方法會釋放同步監視器
  • Thread.sleep(),Thread.yield()不會釋放同步監視器

同步鎖(Lock)

java5開始提供了同步鎖,經常使用的同步鎖是ReentrantLock。

同步鎖繼承關係以下:

- Lock
    |-ReentrantLock
- ReadWriteLock
    |-ReentrantReadWriteLock

同步鎖使用代碼片斷:

import java.util.concurrent.locks.*;

public class Account
{
    // 定義鎖對象
    private final ReentrantLock lock = new ReentrantLock();

    // 提供一個線程安全draw()方法來完成取錢操做
    public void draw(double drawAmount)
    {
        // 加鎖
        lock.lock();
        try
        {

        }
        finally
        {
            // 修改完成,釋放鎖
            lock.unlock();
        }
    }
}

ThreadLocal

原理:爲每個使用該變量的線程都提供一個變量值的副本,使每個線程均可以獨立改變本身的副本,而不會和其餘線程的副本衝突,就好像每個線程都徹底擁有該變量同樣。代碼內部使用ThreadLocal.ThreadLocalMap來存儲變量的鍵和值,也就是內部使用一個相似Map的結構來存儲name和name對應的值。

ThreadLocal只提供三個public方法:

  • T get():返回此線程局部變量中當前線程副本中的值。
  • void remove():刪除此線程局部變量中當前線程副本中的值。
  • void set(T value):設置此線程局部變量中當前線程副本中的值。

示例:

class Acount
{
    private ThreadLocal<String> name = new ThreadLocal<>();
    public Acount(String str){
        this.name.set(str);
    }
    public String getName(){
        return name.get();
    }
    public void setName(String str){
        this.name.set(str);
    }
    //......
}

線程間通訊

傳統線程間通訊

適用條件:若是程序中使用的是同步代碼塊、同步方法來控制線程安全,則線程間的通訊是使用Object類的 wait(),notify(),notifyAll()控制線程間通訊。

public class Account
{
    // 封裝帳戶編號、帳戶餘額的兩個成員變量
    private String accountNo;
    private double balance;
    // 標識帳戶中是否已有存款的旗標
    private boolean flag = false;

    public Account(){}
    // 構造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法,
    public double getBalance()
    {
        return this.balance;
    }

    public synchronized void draw(double drawAmount)
    {
        try
        {
            // 若是flag爲假,代表帳戶中尚未人存錢進去,取錢方法阻塞
            if (!flag)
            {
                wait();
            }
            else
            {
                // 執行取錢
                System.out.println(Thread.currentThread().getName()
                    + " 取錢:" +  drawAmount);
                balance -= drawAmount;
                System.out.println("帳戶餘額爲:" + balance);
                // 將標識帳戶是否已有存款的旗標設爲false。
                flag = false;
                // 喚醒其餘線程
                notifyAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
    }
    public synchronized void deposit(double depositAmount)
    {
        try
        {
            // 若是flag爲真,代表帳戶中已有人存錢進去,則存錢方法阻塞
            if (flag)             //①
            {
                wait();
            }
            else
            {
                // 執行存款
                System.out.println(Thread.currentThread().getName()
                    + " 存款:" +  depositAmount);
                balance += depositAmount;
                System.out.println("帳戶餘額爲:" + balance);
                // 將表示帳戶是否已有存款的旗標設爲true
                flag = true;
                // 喚醒其餘線程
                notifyAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
    }
}

使用Condition控制線程間通訊

適用條件:若是程序中使用的是同步鎖(Lock)來控制線程安全,則線程間通訊是使用Condition來控制線程間通訊。

獲取Condiion實例代碼片斷:經過鎖去獲取

private final Lock lock = new ReentrantLock();
        private final Condition condition = lock.newCondition();

Condition類提供以下方法以實現線程間通訊:

  • await()
  • signal(),喚醒單個線程
  • signalAll(),喚醒此Lock上的全部線程

示例:

import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class Account
{
    // 顯式定義Lock對象
    private final Lock lock = new ReentrantLock();
    // 得到指定Lock對象對應的Condition
    private final Condition cond  = lock.newCondition();

    public void draw(double drawAmount)
    {
        // 加鎖
        lock.lock();
        try
        {
            if ()
            {
                
            }
            else
            {
                // 喚醒其餘線程
                cond.signalAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        // 使用finally塊來釋放鎖
        finally
        {
            lock.unlock();
        }
    }
}

使用阻塞隊列(BockingQueue)控制線程間通訊

todo...

線程組與異常處理

todo...

線程池

普通線程池

java 5 新增了一個 Executors 工廠類來產生線程池,經常使用如下幾個靜態方法:

  • ExecutorService newCachedThreadPool():系統根據須要建立一個具備緩存功能的線程池。
  • ExecutorService newFixedThreadPool(int nThreads):建立一個可重用的、固定線程數的線程池。
  • ExecutorService newSingleThreadExector():建立一個只有單個線程的線程池。
  • ScheduledExecutorService newScheduledThreadPool(int corePoolSize):建立指定線程數的線程池,它能夠指定延遲後執行線程任務。
  • ScheduledExecutorService newSingleThreadScheduledExecutor():建立一個只有一個線程和線程池,它能夠在指定延遲後執行線程任務。
  • ExecutorService newWorkStealingPool(int parallelism):建立持有足夠的線程的線程池來支持給定的並行級別,該方法還會使用多個隊列來減小競爭。(java 8)
  • ExecutorService newWorkStealingPool():該方法是前一個方法的簡化版本。若是當前機器有4個CPU,則目標並行級別被設置爲4,也就是至關於前一個方法傳入4做爲參數。(java 8)

ExecutorService 表明儘快執行線程的線程池,程序只需把一個 Runnable 對象或 Callable 對象提交給指定的線程池便可。

ExecutorService 經常使用如下方法:

  • Future<?> submit(Runnable task):將一個線程任務提交給線程池,Future表明返回值。
  • <T> Future<T> submit(Runnable task,T result):同上,其中result顯式指定線程執行執行結束後返回的值。
  • <T> Future<T> submit(Callable<T> task):將一個Calable對象提交給指定的線程池,Future表明返回值。

線程池使用步驟:
一、調用Executors類的靜態工廠方法建立一個ExecutorService對象。
二、建立Runnable實現類或Callable實現類的實例。
三、調用ExecutorService對象的submit()方法提交Runnable或Callable實例。
四、調用ExecutorService對象的shutdown()關閉線程池。

示例代碼片斷:

import java.util.concurrent.*;

public class ThreadPoolTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 建立足夠的線程來支持4個CPU並行的線程池
        // 建立一個具備固定線程數(6)的線程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 使用Lambda表達式建立Runnable對象
        Runnable target = () -> {
            for (int i = 0; i < 100 ; i++ )
            {
                System.out.println(Thread.currentThread().getName()
                    + "的i值爲:" + i);
            }
        };
        // 向線程池中提交兩個線程
        pool.submit(target);
        pool.submit(target);
        // 關閉線程池
        pool.shutdown();
    }
}

ForkJoinPool

jdk 7 提供了ForkJoinPool來支持將一個任務拆分紅多個「小任務」並行計算,再把多個「小任務」的結果合併成總的計算結果。

java.util.concurrent.Executor
                    |-ExecutorService
                        |-AbstractExecutorService
                            |-ForkJoinPool

建立ForkJoinPool實例後,就能夠調用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法來提交任務。

其中ForkJoinTask有兩個抽象子類:

java.util.concurrent.Future
                    |-ForkJoinTask          表示一個可執行、合併的任務
                        |-RecursiveAction   表示沒有返回值的任務
                        |-RecursiveTask     表示有返回值的任務

完整示例:

import java.util.concurrent.*;

// 繼承RecursiveAction來實現"可分解"的任務
class PrintTask extends RecursiveAction
{
    // 每一個「小任務」只最多隻打印50個數
    private static final int THRESHOLD = 50;
    private int start;
    private int end;
    // 打印從start到end的任務
    public PrintTask(int start, int end)
    {
        this.start = start;
        this.end = end;
    }
    @Override
    protected void compute()
    {
        // 當end與start之間的差小於THRESHOLD時,開始打印
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                System.out.println(Thread.currentThread().getName()
                    + "的i值:" + i);
            }
        }
        else
        {
            // 若是當end與start之間的差大於THRESHOLD時,即要打印的數超過50個
            // 將大任務分解成兩個小任務。
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            // 並行執行兩個「小任務」
            left.fork();
            right.fork();
        }
    }
}
public class ForkJoinPoolTest
{
    public static void main(String[] args)
        throws Exception
    {
        ForkJoinPool pool = new ForkJoinPool();
        // 提交可分解的PrintTask任務
        pool.submit(new PrintTask(0 , 300));
        pool.awaitTermination(2, TimeUnit.SECONDS);
        // 關閉線程池
        pool.shutdown();
    }
}
import java.util.concurrent.*;
import java.util.*;

// 繼承RecursiveTask來實現"可分解"的任務
class CalTask extends RecursiveTask<Integer>
{
    // 每一個「小任務」只最多隻累加20個數
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;
    // 累加從start到end的數組元素
    public CalTask(int[] arr , int start, int end)
    {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute()
    {
        int sum = 0;
        // 當end與start之間的差小於THRESHOLD時,開始進行實際累加
        if(end - start < THRESHOLD)
        {
            for (int i = start ; i < end ; i++ )
            {
                sum += arr[i];
            }
            return sum;
        }
        else
        {
            // 若是當end與start之間的差大於THRESHOLD時,即要累加的數超過20個時
            // 將大任務分解成兩個小任務。
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr , start, middle);
            CalTask right = new CalTask(arr , middle, end);
            // 並行執行兩個「小任務」
            left.fork();
            right.fork();
            // 把兩個「小任務」累加的結果合併起來
            return left.join() + right.join();    // ①
        }
    }
}
public class Sum
{
    public static void main(String[] args)
        throws Exception
    {
        int[] arr = new int[100];
        Random rand = new Random();
        int total = 0;
        // 初始化100個數字元素
        for (int i = 0 , len = arr.length; i < len ; i++ )
        {
            int tmp = rand.nextInt(20);
            // 對數組元素賦值,並將數組元素的值添加到sum總和中。
            total += (arr[i] = tmp);
        }
        System.out.println(total);
        // 建立一個通用池
        ForkJoinPool pool = ForkJoinPool.commonPool();
        // 提交可分解的CalTask任務
        Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
        System.out.println(future.get());
        // 關閉線程池
        pool.shutdown();
    }
}
相關文章
相關標籤/搜索