Java多線程(下)

線程同步

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

線程同步:java

即當有一個線程在對內存進行操做時,其餘線程都不能夠對這個內存地址進行操做,直到該線程完成操做, 其餘線程才能對該內存地址進行操做,而其餘線程又處於等待狀態,實現線程同步的方法有不少,臨界區對象就是其中一種。git

在通常狀況下,建立一個線程是不能提升程序的執行效率的,因此要建立多個線程。可是多個線程同時運行的時候可能調用線程函數,在多個線程同時對同一個內存地址進行寫入,因爲CPU時間調度上的問題,寫入數據會被屢次的覆蓋,因此就要使線程同步。算法

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

「同」字從字面上容易理解爲一塊兒動做安全

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

如進程、線程同步,可理解爲進程或線程A和B一塊配合,A執行到必定程度時要依靠B的某個結果,因而停下來,示意B運行;B依言執行,再將結果給A;A再繼續操做。併發

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

在多線程編程裏面,一些敏感數據不容許被多個線程同時訪問,此時就使用同步訪問技術,保證數據在任什麼時候刻,最多有一個線程訪問,以保證數據的完整性。ide

因爲同一進程的多個線程共享同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突問題,爲了保證數據在方法中被訪問時的正確性,在訪問時加入鎖機制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多線程(上)http://www.javashuo.com/article/p-cjqayqvb-wr.html

查看原文

相關文章
相關標籤/搜索