Java多執行緒(下)

執行緒同步

當多個執行緒訪問一個對象時,有可能會發生污讀,即讀取到未及時更新的數據,這個時候就需要執行緒同步。

執行緒同步:

即當有一個執行緒在對記憶體進行操作時,其他執行緒都不可以對這個記憶體地址進行操作,直到該執行緒完成操作, 其他執行緒才能對該記憶體地址進行操作,而其他執行緒又處於等待狀態,實現執行緒同步的方法有很多,臨界區對象就是其中一種。

在一般情況下,創建一個執行緒是不能提高程式的執行效率的,所以要創建多個執行緒。但是多個執行緒同時運行的時候可能調用執行緒函數,在多個執行緒同時對同一個記憶體地址進行寫入,由於CPU時間調度上的問題,寫入數據會被多次的覆蓋,所以就要使執行緒同步。

同步就是協同步調,按預定的先後次序進行運行。如:你說完,我再說。

「同」字從字面上容易理解為一起動作

其實不是,「同」字應是指協同、協助、互相配合。

如進程、執行緒同步,可理解為進程或執行緒A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B運行;B依言執行,再將結果給A;A再繼續操作。

所謂同步,就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回,同時其它執行緒也不能調用這個方法。按照這個定義,其實絕大多數函數都是同步調用(例如sin, isdigit等)。但是一般而言,我們在說同步、非同步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。例如Window API函數SendMessage。該函數發送一個消息給某個窗口,在對方處理完消息之前,這個函數不返回。當對方處理完畢以後,該函數才把消息處理函數所返回的LRESULT值返回給調用者。

在多執行緒編程裡面,一些敏感數據不允許被多個執行緒同時訪問,此時就使用同步訪問技術,保證數據在任何時刻,最多有一個執行緒訪問,以保證數據的完整性。

由於同一進程的多個執行緒共享同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證數據在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得對象的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可能存在以下問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起;
  • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和調度延時,引
    起性能問題;
  • 如果一個優先順序高的執行緒等待- -個優先順序低的執行緒釋放鎖會導致優先順序倒
    置,引起性能問題.

舉個例子,一個售票口有10張票,當100個人同時去買時,每個人都獲取到了有100張票的數據,所以每個人買了一張,導致最後剩下-90張票,執行緒不同步就會導致這種結果。

synchronized

synchronized是Java中的關鍵字,是一種同步鎖。它修飾的對象有以下幾種:

  1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括弧{}括起來的程式碼,作用的對象是調用這個程式碼塊的對象;
  2. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
  3. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象;
  4. 修改一個類,其作用的範圍是synchronized後面括弧括起來的部分,作用主的對象是這個類的所有對象。

我們寫一個例子,使用執行緒不安全的List來看看效果

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

可以看到,循環1000次,只存進去998個,重複執行,這個大小還會變化,所以是執行緒不安全的。

可以使用synchronized把list加鎖,就能保證每次都能插入進去。

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               synchronized (list) {
                   list.add(Thread.currentThread().getName());
               }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

這樣就能夠保證執行緒安全。

也可以使用JUC(java.util.concurrent)包下的執行緒安全的列表CopyOnWriteArrayList,程式碼如下

import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

使用CopyOnWriteArrayList就可以不需要synchronized關鍵字實現執行緒安全

查看源程式碼可以發現,CopyOnWriteArrayList實現了List<E>介面

然後再add方法中使用了synchronized來加鎖,和我們上面的操作方法一致

//CopyOnWriteArrayList中的add()方法
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

死鎖

所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。

死鎖的條件

  • 互斥條件
  • 請求和保持
  • 不可搶佔
  • 循環等待

只要破壞後三個條件之一就可以避免死鎖,可以使用銀行家演算法等方法。

Lock鎖

  • 從JDK 5.0開始,Java提供了更強大的執行緒同步機制一通過顯式定義同步鎖對象來實現同步。同步鎖使用Lock對象充當java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。
  • Lock鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock對象加鎖,執行緒開
    始訪問共享資源之前應先獲得Lock對象
  • ReentrantLock類實現了Lock,它擁有與synchronized相同的並發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。

先寫一個不使用鎖的例子

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);

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

    }
    public static int tickets = 10;
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(tickets--);
            } else {
                break;
            }
        }
    }
}

執行後發現順序完全是亂的

使用ReentrantLock(可重入鎖)來把相關程式碼加鎖,即可實現按順序調用

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);

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

    }
    public static int tickets = 10;
    final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets > 0) {
                    System.out.println(tickets--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

這樣也可以實現執行緒同步。

  • Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖) synchronized是隱式鎖,出了
    作用域自動釋放
  • Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
  • 使用Lock鎖, JVM將花費較少的時間來調度執行緒,性能更好。並且具有更好的擴展
    性(提供更多的子類)。
  • 優先使用順序:
    • Lock >同步程式碼塊(已經進入了方法體,分配了相應資源) >同步方法(在方
      法體之外)

執行緒通訊

生產者和消費者問題

  • 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費。
  • 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止。
  • 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止。

Java提供的執行緒通訊方法

方法名 作用
wait() 表示執行緒一直等待,直到其他執行緒通知,與sleep不同,會釋放鎖
wait(long timeout) 指定等待的毫秒數
notify() 喚醒一個處於等待狀態的執行緒
notifyAll() 喚醒同一個對象上所有調用wait()方法的執行緒,優先順序別高的執行緒優先調度

均是0bject類的方法都,只能在同步方法或者同步程式碼塊中使用,否則會拋出llegalMonitorStateException

  • 對於生產者,沒有生產產品之前,要通知消費者等待.而生產了產品之後,又需要馬_上通知消費者消費
  • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費
  • 在生產者消費者問題中,僅有synchronized是不夠的
    • synchronized 可阻止並發更新同- -個共享資源,實現了同步
    • synchronized 不能用來實現不同執行緒之間的消息傳遞(通訊)

解決方式一:管程

首先定義一個生產者類

//生產者
class Producer extends Thread {
    SynContainer container;
    public Producer(SynContainer container) {
        this.container = container;
    }

    //生產
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生產第" + i + "個");
            container.push(new Product(i));
        }
    }
}

生產者不斷往緩衝區添加產品,然後定義一個消費者類

//消費者
class Consumer extends Thread {
    SynContainer container;
    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消費
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消費第" + container.pop().id + "個");
            try {
                Thread.sleep(500);
            } catch (InterruptedException ignored) { }
        }
    }
}

消費者不斷在緩衝區去除產品,這裡添加一個sleep來模擬真實效果

最後定義緩衝區

//緩衝區
class SynContainer {
    //容器大小
    Product[] products = new Product[10];
    //計數器
    int count = 0;

    //生產者放入產品
    public synchronized void push(Product product) {
        //如果滿了,通知消費者,生產者等待,否則放入產品
        if (count == products.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[count++] = product;
        this.notifyAll();
    }
    //消費者消費產品
    public synchronized Product pop() {
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        return products[--count];
    }
}

緩衝區的兩個方法都是使用synchronized修飾,保證能夠執行完整,然後根據容器大小來判斷是否讓生產者以及消費者執行緒等待

當容器中沒有產品時,通知消費者等待,生產者執行緒開始,當產品滿時,通知生產者等待,消費者執行緒開始。

最後補上產品類

//產品
class Product {
    //產品編號
    int id;

    public Product(int id) {
        this.id = id;
    }
}

解決方式二:訊號量

類定義和上面類似,只不過在產品類中添加了一個訊號量來區分是否有產品,不需要一個緩衝區

//生產者
class Producer extends Thread {
    Product product;

    public Producer(Product product) {
        this.product = product;
    }

    //生產
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.product.push("產品" + i);
        }
    }
}

//消費者
class Consumer extends Thread {
    Product product;

    public Consumer(Product product) {
        this.product = product;
    }

    //消費
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.product.pop();
        }
    }
}

//產品
class Product {
    String product;
    boolean flag = true;

    //生產
    public synchronized void push(String product) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException ignored) { }
        }
        System.out.println("生產了" + product);
        //通知消費
        this.notifyAll();
        this.product = product;
        this.flag = !this.flag;

    }

    //消費
    public synchronized void pop() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException ignored) { }
        }
        System.out.println("消費了" + this.product);
        //通知生產者
        this.notifyAll();
        this.flag = !this.flag;
    }
}

這樣也可以解決生產者和消費者問題

執行緒池

背景

經常創建和銷毀、使用量特別大的資源,比如並發情況下的執行緒,對性能影響很大。

思路:提前創建好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀、實現重複利用。類似生活中的公共交通工具。

優點

  • 提高響應速度(減少了創建新執行緒的時間)
  • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都創建)
  • 便於執行緒管理

參數說明

  • corePoolSize: 核心池的大小
  • maximumPoolSize:最大執行緒數
  • keepAliveTime: 執行緒沒有任務時最多保持多長時間後會終止

JDK 5.0起提供了執行緒池相關API: ExecutorService和Executors
ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor

  • void execute(Runnable command) :執行任務/命令,沒有返回值,-般用來執行Runnable
  • <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一-般 又來執行
    Callable
  • void shutdown() :關閉連接池
    Executors:工具類、執行緒池的工廠類,用於創建並返回不同類型的執行緒池

程式碼演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        //創建執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //關閉連接
        service.shutdown();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

這樣就可以實現通過執行緒池來管理執行緒

總結

  • 執行緒就是獨立的執行路徑;
  • 在程式運行時,即使沒有自己創建執行緒,後台也會有多個執行緒,如主執行緒,gc執行緒;
  • main()稱之為主執行緒,為系統的入口,用於執行整個程式;
  • 在一個進程中,如果開闢了多個執行緒,執行緒的運行由調度器安排調度,調度器是與
  • 作業系統緊密相關的,先後順序是不能認為的干預的。
  • 對同一份資源操作時,會存在資源搶奪的問題,需要加入並發控制;
  • 執行緒會帶來額外的開銷,如cpu調度時間,並發控制開銷。
  • 每個執行緒在自己的工作記憶體交互,記憶體控制不當會造成數據不一致

Java多執行緒(上)//www.cnblogs.com/chaofanq/p/15024558.html

查看原文

Tags: