深入理解Java多線程與並發編程


 

課程目標

線程三大特性

       Java內存模型

       Volatile

       ThreadLoca

       線程

線程三大特性

多線程三大特性,原子性、可見性有序

2.1 什么是原子性

即一個操作或者多個操作 要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行。

一個很經典的例子就是銀行賬戶轉賬問題: 
比如從賬戶A向賬戶B1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

我們操作數據也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。 

原子性其實就是保證數據一致、線程安全一部分,

2.3 什么可見性

當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

若兩個線程在不同的cpu,那么線程1改變了i的值還沒刷新到主存,線程2又使用了i,那么這個i值肯定還是之前的,線程1對變量的修改線程沒看到這就是可見性問題。 

2.3什么是有序性

程序執行的順序按照代碼的先后順序執行。

一般來說處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。如下:

int a = 10;    //語句1

int r = 2;    //語句2

a = a + 3;    //語句3

r = a*a;     //語句4

則因為重排序,他還可能執行順序為 2-1-3-41-3-2-4
但絕不可能 2-1-4-3,因為這打破了依賴關系。
顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。

Java內存模型

共享內存模型指的就是Java內存模型(簡稱JMM)JMM決定一個線程對共享變量的寫入時,對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。

從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:

1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2. 然后,線程B到主內存中去讀取線程A之前已更新過的共享變量。

下面通過示意圖來說明這兩個步驟: 

如上圖所示,本地內存AB有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都為0。線程A在執行時,把更新后的x值(假設值為1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改后的x值刷新到主內存中,此時主內存中的x值變為了1。隨后,線程B到主內存中去讀取線程A更新后的x值,此時線程B的本地內存的x值也變為了1

從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為java程序員提供內存可見性保證。

總結:什么是Java內存模型:java內存模型簡稱jmm義了一個線程另一個線程可見。共享變量存放在主內存中,每個線程都有自己的本地內存,多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,所以就會發生線程安全問題。

Volatile

4.1、什么Volatile

Volatile 關鍵字的作用是變量在多個線程之間可見。

代碼:

class ThreadVolatileDemo extends Thread {
    public    boolean flag = true;
    @Override
    public void run() {
        System.out.println("開始執行子線程....");
        while (flag) {
        }
        System.out.println("線程停止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }

}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設置成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);

    }
}

 

 運行結果:

 

 

 

已經將結果設置為fasle為什么?還一直在運行呢。

原因:線程之間是不可見的,讀取的是副本,沒有及時讀取到主內存結果。

解決辦法使用Volatile關鍵字將解決線程之間可見性, 強制線程每次讀取該值的時候都去“主內存”中取值

 

4.2 Volatile非原子性

public class VolatileNoAtomic extends Thread {
    private static volatile int count;

    // private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount() {
        for (int i = 0; i < 1000; i++) {
            count++;
            // count.incrementAndGet();
        }
        System.out.println(count);
    }

    public void run() {
        addCount();
    }

    public static void main(String[] args) {

        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }

        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }

}

運行結果:

 

 

 

 

 

結果發現 數據不同步,因為Volatile不用具備原子性。

4.3、使用AtomicInteger原子類

 

public class VolatileNoAtomic extends Thread {
    static int count = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //等同於i++
            atomicInteger.incrementAndGet();
        }
        System.out.println(count);
    }

    public static void main(String[] args) {
        // 初始化10個線程
        VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
        for (int i = 0; i < 10; i++) {
            // 創建
            volatileNoAtomic[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < volatileNoAtomic.length; i++) {
            volatileNoAtomic[i].start();
        }
    }

}

 

4.4volatilesynchronized區別

僅靠volatile不能保證線程的安全性。(原子性)

①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法

②volatile只能保證數據的可見性,不能用來同步,因為多個線程並發訪問volatile修飾的變量不會阻塞。

synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的線程才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。

線程安全性

線程安全性包括兩個方面,可見性。原子性。

從上面自增的例子中可以看出:僅僅使用volatile並不能保證線程安全性。而synchronized則可實現線程的安全性。

ThreadLocal

5.1、什么ThreadLocal

 ThreadLocal提高一個線程的局部變量,訪問某個線程擁有自己局部變量。

 當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

ThreadLocal的接口方法

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:

  • void set(Object value)設置當前線程的線程局部變量的值。
  • public Object get()該方法返回當前線程所對應的線程局部變量。
  • public void remove()將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。
  • protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null

 

案例:創建三個線程,每個線程生成自己獨立序列號

代碼:

 

class Res {
    // 生成序列號共享變量
    public static Integer count = 0;
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {

            return 0;
        };

    };

    public Integer getNum() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }
}

public class ThreadLocaDemo2 extends Thread {
    private Res res;

    public ThreadLocaDemo2(Res res) {
        this.res = res;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
        }

    }

    public static void main(String[] args) {
        Res res = new Res();
        ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
        threadLocaDemo1.start();
        threadLocaDemo2.start();
        threadLocaDemo3.start();
    }

}

 

5.2ThreadLoca實現原理

ThreadLoca通過map集合

Map.put(“當前線程”,值);

 

 

、線程池

1.1、什么是線程池?

 線程池是指在初始化一個多線程應用程序過程中創建一個線程集合,然后在需要執行新的任務時重用這些線程而不是新建一個線程。線程池中線程的數量通常完全取決於可用內存數量和應用程序的需求。然而,增加可用線程數量是可能的。線程池中的每個線程都有被分配一個任務,一旦任務已經完成了,線程回到池子中並等待下一次分配任務。

1.2、線程池作用

基於以下幾個原因在多線程應用程序中使用線程是必須的:

  1. 線程池改進了一個應用程序的響應時間。由於線程池中的線程已經准備好且等待被分配任務,應用程序可以直接拿來使用而不用新建一個線程。

  2. 線程池節省了CLR 為每個短生存周期任務創建一個完整的線程的開銷並可以在任務完成后回收資源。

  3. 線程池根據當前在系統中運行的進程來優化線程時間片。

  4. 線程池允許我們開啟多個任務而不用為每個線程設置屬性。

  5. 線程池允許我們為正在執行的任務的程序參數傳遞一個包含狀態信息的對象引用。

  6. 線程池可以用來解決處理一個特定請求最大線程數量限制問題。

1.3 、 線程池四種創建方式

Java通過Executorsjdk1.5並發包)提供四種線程池,分別為:
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

1.1  newCachedThreadPool

創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。示例代碼如下:

 

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            // try {
            // Thread.sleep(index * 1000);
            // } catch (InterruptedException e) {
            // e.printStackTrace();
            // }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "---" + index);
                }
            });
        }

 

總結: 線程池為無限大,當執行第二個任務時第一個任務已經完成,會復用執行第一個任務的線程,而不用每次新建線程。

1.2  newFixedThreadPool

創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。示例代碼如下:

 

// 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待
        final ExecutorService newCachedThreadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            final int index = i;
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println("i:" + index);
                }
            });
        }

 

 

 

總結:因為線程池大小為3,每個任務輸出index后sleep 2秒,所以每兩秒打印3個數字。

定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()

1.3  newScheduledThreadPool

創建一個定長線程池,支持定時及周期性任務執行。延遲執行示例代碼如下:

 

// 創建一個定長線程池,支持定時及周期性任務執行。延遲執行示例代碼如下:
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
        newScheduledThreadPool.schedule(new Runnable() {
            public void run() {
                System.out.println("delay 3 seconds");
            }
        }, 3, TimeUnit.SECONDS);

 

表示延遲3秒執行。

1.4  newSingleThreadExecutor

創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。示例代碼如下:

 

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            newSingleThreadExecutor.execute(new Runnable() {

                @Override
                public void run() {
                    System.out.println("index:" + index);
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            });
        }

 

 

 

注意結果依次輸出,相當於順序執行各個任務。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM