多線程實踐

Java 知識目錄

創建多線程

繼承 Thread 類

Thread 常用構造方法中,常用的有兩個參數 Thread(Runnable target, String name) Runnable接口的實現類,以及線程名稱

/**
 * 通過繼承 Thread 創建多線程
 * 1. 創建一個Thread類的子類
 * 2. 在Thread子類中重寫Thread類中的run方法,設置線程任務
 * 3. 調用Thread類中的start方法,開啓新線程,執行run方法
 */
public class ExtendThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("ExtendThread" + i);
        }
    }

    /**
     * 運行結果
     * ExtendThread0
     * main0
     * ExtendThread1
     * main1
     * ExtendThread2
     * main2
     */
    public static void main(String[] args) {
        ExtendThread extendThread = new ExtendThread();
        extendThread.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("main" + i);
        }

    }
}

實現 Runnable 接口

/*
 * 創建`Runnable` 接口的實現類
 * 在實現類中重寫 `Runnable` 接口的 `run` 方法,設置線程任務
 * 創建`Runnable` 接口的實現類對象
 * 創建`Thread`類對象,構造方法中傳遞`Runnable` 接口的實現類對象
 * 調用`Thread` 類中的 `start`方法,開啓新的線程執行 `run`方法
 *

 * */
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        Thread thread = new Thread(runnable);

        thread.start();
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

Thread 的常用方法

package functions;

/**
*  static Thread currentThread() 		返回對當前正在執行的線程對象的引用。
*  String getName() 					返回此線程的名稱。
*  void setName(String name) 			將此線程的名稱更改爲等於參數 name 。
*  static void sleep(long millis) 		使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行),使用 Thread 直接調用即可
*  void start() 						導致此線程開始執行; Java虛擬機調用此線程的run方法。
 */
public class ThreadFunction implements Runnable{
    @Override
    public void run() {
        System.out.println("ThreadFunction:" + Thread.currentThread().getName());
        //延遲100ms
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new ThreadFunction();
        Thread thread = new Thread(runnable);

        //添加了延時函數,所以main函數線程會先執行
        thread.start();

        System.out.println("main:" + Thread.currentThread().getName());
        //修改main函數線程的名稱
        Thread.currentThread().setName("main2");
        System.out.println("main2:" + Thread.currentThread().getName());


    }
}

Thread 與 Runnable 的比較

實際開發中一般使用Runnable接口的方式比較多,因爲:
通過繼承Thread類的方式,可以完成多線程的建立。但是這種方式有一個侷限性,如果一個類已經有了自己的父類,就不可以繼承Thread類。而實現Runnable接口可以避免單繼承的侷限性。

匿名內部類的方式實現(不再敘述)

synchronized 關鍵字

  • synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。所以現在的synchronized鎖效率也優化得很不錯了。

synchronized 使用方式

修飾一個代碼塊

  • 其作用的範圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象
/**
 * Synchronized 修飾一個代碼塊
 * 當兩個併發線程(thread1和thread2)訪問同一個對象(syncThread)中的synchronized代碼塊時,
 * 在同一時刻只能有一個線程得到執行,另一個線程受阻塞,必須等待當前線程執行完這個代碼塊
 * 以後才能執行該代碼塊。Thread1和thread2是互斥的,因爲在執行synchronized代碼塊時會鎖定
 * 當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。
 */
public class SynchronizedBlock implements Runnable{

    private static int count = 0;//計數

    @Override
    public void run() {
        synchronized (this){
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    public static void main(String[] args) {

        SynchronizedBlock synchronizedBlock = new SynchronizedBlock();
        Thread thread1 = new Thread(synchronizedBlock,"SynchronizedBlock1");
        Thread thread2 = new Thread(synchronizedBlock,"SynchronizedBlock2");

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

    }
}

修飾一個方法

  • 其作用的範圍是整個方法,作用的對象是調用這個方法的對象

修飾一個靜態方法

  • 其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象

修飾一個類

  • 其作用的範圍是synchronized後面括號括起來的部分,作用主的對象是這個類的所有對象

volatile 關鍵字

  • 保證變量的可見性然後還有一一個作用是防止指令重排序。

synchronized關鍵字和volatile關鍵字比較

  • volatile關鍵字是線程同步的輕量級實現,所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用於變量而synchronized關鍵字可以修飾方法以及代碼塊。
  • synchronized關鍵字 在JavaSE1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用synchronized關鍵字的場景還是更多-些。
  • 多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
  • volatile關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized關鍵字兩者都能保證。
  • volatile關鍵字主要用於解決變量在多個線程之間的可見性,而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。

Actomic

  • Atomic是指一個 操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始, 就不會被其他線程干擾。所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。
  • 基本類型
    • AtomicInteger:整形原子類
    • AtomicLong:長整型原子類
    • AtomicBoolean:布爾型原子類

**AtomicInteger **使用示例:

class AtomicIntegerTest {
	private AtomicInteger count = new AtomicInteger();
	//使⽤AtomicInteger之後,不需要對該⽅法加鎖,也可以實現線程安全。
	public void increment() {
		count.incrementAndGet();
	}
	public int getCount() {
		return count.get();
	}
}

ThreadLocal

  • 其它三個關鍵字都是從線程外來保證變量的一致性,這樣使得多個線程訪問的變量具有一致性,解決資源共享的問題。而ThreadLocal的設計,是用來提供線程內的局部變量,這樣每個線程都自己管理自己的局部變量,別的線程操作的數據不會對本線程產生影響。
 * @Author 安仔夏天很勤奮
 * Create Date is  2019/3/21
 *
 * 描述 Java中的ThreadLocal類允許我們創建只能被同一個線程讀寫的變量。
 * 因此,如果一段代碼含有一個ThreadLocal變量的引用,即使兩個線程同時執行這段代碼,
 * 它們也無法訪問到對方的ThreadLocal變量。
 */
public class ThreadLocalExsample {/**
 * 創建了一個MyRunnable實例,並將該實例作爲參數傳遞給兩個線程。兩個線程分別執行run()方法,
 * 並且都在ThreadLocal實例上保存了不同的值。如果它們訪問的不是ThreadLocal對象並且調用的set()方法被同步了,
 * 則第二個線程會覆蓋掉第一個線程設置的值。但是,由於它們訪問的是一個ThreadLocal對象,
 * 因此這兩個線程都無法看到對方保存的值。也就是說,它們存取的是兩個不同的值。
 */
 public static class MyRunnable implements Runnable {
 /**
 * 例化了一個ThreadLocal對象。我們只需要實例化對象一次,並且也不需要知道它是被哪個線程實例化。
 * 雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程卻只能訪問到自己通過調用ThreadLocal的
 * set()方法設置的值。即使是兩個不同的線程在同一個ThreadLocal對象上設置了不同的值,
 * 他們仍然無法訪問到對方的值。
 */
 private ThreadLocal threadLocal = new ThreadLocal();
 @Override
 public void run() {
 //一旦創建了一個ThreadLocal變量,你可以通過如下代碼設置某個需要保存的值
 threadLocal.set((int) (Math.random() * 100D));
 try {
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 }
 //可以通過下面方法讀取保存在ThreadLocal變量中的值
 System.out.println("-------threadLocal value-------"+threadLocal.get());
 }
 }public static void main(String[] args) {
 MyRunnable sharedRunnableInstance = new MyRunnable();
 Thread thread1 = new Thread(sharedRunnableInstance);
 Thread thread2 = new Thread(sharedRunnableInstance);
 thread1.start();
 thread2.start();
 }
}
​
運行結果
-------threadLocal value-------38
-------threadLocal value-------88

線程池

  • 線程池提供了一種限制和管理資源(包括執行一個任務)。每個線程池還維護一些基本統計信息,例如已完成任務的數量。

使用線程池的好處

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

執行execute()方法和submit()方法的區別是什麼呢?

1.execute()方 法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;
2. submit()方法用於提交需要返回值的任務。線程池會返回一個Future類型的對象,通過這個Future對象可以判斷任務是否執行成功,並且可以通過Futureget()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get (long t imeout, TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

創建一個線程池—— ThreadPoolExecutor

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {
    //使⽤阿⾥巴巴推薦的創建線程池的⽅式
    //通過ThreadPoolExecutor構造函數⾃定義參數創建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,     //最小可以同時運行的線程數量
                MAX_POOL_SIZE,      //線程最大連接數
                KEEP_ALIVE_TIME,    //回收等待時間爲1L
                TimeUnit.SECONDS,   //等待時間單位爲分鐘
                new ArrayBlockingQueue<>(QUEUE_CAPACITY), //任務隊列爲ArrayBlockingQueue,並且容量爲100;
                new ThreadPoolExecutor.CallerRunsPolicy());//飽和策略爲CallerRunsPolicy
        for (int i = 0; i < 10; i++) {
            //創建WorkerThread對象(WorkerThread類實現了Runnable 接⼝)
            Runnable worker = new MyRunnable();
            //執⾏Runnable
            executor.execute(worker);
        }
        //終⽌線程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

補充

併發編程的三個重要特性

  1. 原子性:一個的操作或者多次操作,要麼所有的操作全部都得到執行並且不會收到任何因素的干擾而中斷,要麼所有的操作都執行,要麼都不執行。synchronized可以保證代碼片段的原子性。
  2. 可見性:當一個變量對共享變量進行了修改,那麼另外的線程都是立即可以看到修改後的最新值。volatile 關鍵字可以保證共享變量的可見性。
  3. 有序性:代碼在執行的過程中的先後順序,Java在編譯器以及運行期間的優化,代碼的執行順序未必就是編寫代碼時候的順序。volatile 關鍵字可以禁止指令進行重排序優化。

參考文獻:
https://www.cnblogs.com/fnlingnzb-learner/p/10335662.html
https://www.jianshu.com/p/6fc3bba12f38
https://blog.csdn.net/u010687392/article/details/50549236
https://github.com/Snailclimb/JavaGuide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章