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多線程(上)https://www.cnblogs.com/chaofanq/p/15024558.html

查看原文


免責聲明!

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



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