線程安全解決-鎖-併發包

線程安全解決-鎖-併發包

爲了解決多線程安全問題,java官方提供了鎖的概念,還有一些併發包,下面讓我們來認識一下吧。

AtomicInteger類實現原理
上一章我們說到了AtomicInteger類它的底層採用樂觀鎖的概念那麼什麼是樂觀鎖,就是在線程運行之前,先把自己線程棧中的變量副本與靜態區中的變量副本進行比較,如果一樣就不需要獲取值了,如果不一樣就需要到靜態區中在獲取一次變量副本,從而保證了數據的原子性。下面用一張圖來展示樂觀鎖的大致原理。
在這裏插入圖片描述

下面讓我們看一下對於多線程併發,所產生的的問題,我們以一個例子來說明:
模仿一個賣票系統,有四個人(四個線程)來買票,100張票賣完爲止。

public class Ticket extends Thread {
    private int ticket = 100;

    @Override
    public void run(){
        while (ticket > 0){
            System.out.println(Thread.currentThread().getName() + "***" + ticket);
            ticket--;
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

測試結果:
在這裏插入圖片描述
我們可以看到有些數據已經產生了錯亂,兩個人買到了相同的票,還有的票直接就消失了,買不到,
這就是線程在高併發下所產生的的問題。

synchronized同步代碼塊

synchronized的實現機制是(悲觀鎖),對於synchronized代碼塊每次只能進去一個線程,對數據進行操作,操作完成之後,及時更新靜態區中的變量值,下一次其他線程在運行前就會再次去靜態區中獲取值,這樣保證了數據的安全。

格式:synchronized(任意對象) {可能出現線程安全的代碼}
作用:加上同步代碼塊之後,就不會出現上面發生的問題了。

public class Ticket extends Thread {
    private int ticket = 100;

	//作爲鎖對象傳入到synchronized代碼塊參數中
    Object obj = new Object();

    @Override
    public void run(){
    	//多個線程的鎖對象必須都是同一個對象
        synchronized (obj) {
            while (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "***" + ticket);
                ticket--;
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
    	//創建Thread類對象
        Ticket t = new Ticket();

		//把創建的Thread對象傳入Thread類並且開啓線程
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

測試結果:
在這裏插入圖片描述
我們可以發現加上同步代碼塊以後,已經解決了數據錯亂的問題。

synchronized修飾方法

public class Ticket extends Thread {
    private int ticket = 100;

    @Override
    public synchronized void run(){
        while (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "***" + ticket);
            ticket--;
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

測試結果:
在這裏插入圖片描述

synchronized修飾靜態方法

public class Ticket extends Thread {
    static int ticket = 100;

    public static synchronized void show(){
        while (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "***" + ticket);
            ticket--;
        }
    }

    @Override
    public void run(){
        show();
    }
}


public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

在這裏插入圖片描述

Lock鎖

它與synchronized的作用是一樣的,它是jdk1.5以後出現的,它是一個接口,具體使用看如下代碼。

public class Ticket extends Thread {
    private int ticket = 100;

    //它是一個接口,要new它的子類
    Lock lock = new ReentrantLock();

    @Override
    public void run(){
        lock.lock();    //獲取鎖
        try {
            while (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "***" + ticket);
                ticket--;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();  //釋放鎖 無論是否出現異常都要釋放鎖
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

測試結果:
Thread-1***100
Thread-1***99
Thread-1***98
Thread-1***97
Thread-1***96
Thread-1***95
Thread-1***94
Thread-1***93

synchronized與Lock的方法(等待喚醒機制)

在Object類中我們可以看到又這麼三個方法,可以讓synchronized來使用
wait() 讓當前線程等待
notify() 喚醒當前線程
notifyAll() 喚醒所有線程

Locks接口中也有一個實現類 Condition 提供了一些方法。
await() 讓當前線程等待
signal() 喚醒當前線程
signalAll() 喚醒所有線程

Lock中有一個方法可以獲取 Condition 對象
newCondition() 獲取Condition對象

下面以一個生產者消費者的例子來使用一下:

包子類:

/*
 * JDK1.5以後出現了 Lock鎖
 * Lock lock = new ReentrantLock();獲取鎖對象
 * -----lock.lock() 開啓鎖      lock.unlock() 釋放鎖
 * 以前用 synchronized 同步的方法只能擁有一個鎖對象
 * 現在可以使用 lock.newCondition() 來獲取多個鎖對象 進行對不同線程的業務操作
 * -----await()等待        signal()釋放       signalAll()釋放所有
 */
public class BZ {
    private String name;
    private int count = 1;
    private boolean flag = false;   //存儲商品狀態 如果false就讓生產者運行  true就讓消費者運行

    private Lock lock = new ReentrantLock();    // 創建lock鎖  lock鎖可以創建多個鎖對象(Condition)根據不同的需求對不同的線程進行操作
    private Condition condition_p = lock.newCondition();   //獲取消費者鎖對象
    private Condition condition_c = lock.newCondition();    //獲取生產者鎖對象

    public void set(String name){
        lock.lock();                    //鎖
        try{
            while (flag){               //循環判斷是否符合條件 防止溜掉一些在await方法後被喚醒的線程
                condition_c.await();    //如果有商品 就讓生產者線程等待
            }
            this.name = name + count++; //設置商品名稱 並且加上編號
            System.out.println(Thread.currentThread().getName() + "--生產者做--" + this.name);
            flag = true;                //改變商品狀態
            condition_p.signalAll();    //喚醒所有消費者線程
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();              //無論啥情況 都釋放鎖
        }
    }

    public void get(){
        lock.lock();                    //鎖
        try {
            while (!flag) {             //循環判斷是否符合條件 防止溜掉一些在await方法後被喚醒的線程
                condition_p.await();    //如果沒有商品 就讓消費者線程等待
            }
            System.out.println(Thread.currentThread().getName() + "--消費者吃---------" + this.name);
            flag = false;               //改變商品狀態
            condition_c.signalAll();    //喚醒生產者線程
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();               //無論啥情況 都釋放鎖
        }
    }
}

消費者線程:使用了包子類中的get方法

/**
 * 消費者
 */
public class Producer implements Runnable {

    private BZ bz;
    Producer(BZ bz){    //創建構造方法   讓創建該對象的時候必須傳入一個BZ類
        this.bz = bz;
    }

    @Override
    public void run() {
        while (true){
            bz.get();
        }
    }
}

生產者線程:使用了包子類中的set方法

/**
 * 生產者
 */
public class Consumer implements Runnable {

    private BZ bz;
    Consumer(BZ bz){     //創建構造方法   讓創建該對象的時候必須傳入一個BZ類
        this.bz = bz;
    }

    @Override
    public void run() {
        while (true){
            bz.set("包子");
        }
    }
}

測試類:

public class Demo {
    public static void main(String[] args) {
        BZ bz = new BZ();   //創建包子對象

        Consumer c = new Consumer(bz);  //創建生產者對象
        Producer p = new Producer(bz);  //創建消費者對象

        Thread t1 = new Thread(c);  //構建生產者線程
        Thread t2 = new Thread(c);  //構建生產者線程
        Thread t3 = new Thread(p);  //構建消費者線程
        Thread t4 = new Thread(p);  //構建消費者線程

        //開啓線程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}


運行結果:
Thread-0--生產者做--包子1
Thread-2--消費者吃---------包子1
Thread-0--生產者做--包子2
Thread-2--消費者吃---------包子2
Thread-0--生產者做--包子3
Thread-2--消費者吃---------包子3
Thread-0--生產者做--包子4
Thread-2--消費者吃---------包子4
Thread-0--生產者做--包子5
Thread-2--消費者吃---------包子5
Thread-0--生產者做--包子6
Thread-2--消費者吃---------包子6
Thread-0--生產者做--包子7

併發包:

我們知道在集合中ArrayList、HashSet、HashMap等集合線程都是不安全的,那麼在多線程的情況下,我們該用那些集合吶:

CopyOnWriteArrayList (與ArrayList一樣,但是它是線程安全的)
CopyOnWriteArraySet (與HashSet一樣,但是它是線程安全的)
ConcurrentHashMap (與HashMap一樣,但是它是線程安全的)
Hashtable(與HashMap一樣,但是效率低、被ConcurrentHashMap代替了)

創建方式:其他的就用法與ArrayList集合一樣,就不在一一演示了。Set與Map也是。

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<> ();

下面介紹幾個併發包需要了解的類
CountDownLatch: 就是在創建CountDownLatch對象的時候給它傳入一個值,該值是int類型,意思就是當該對象的int值爲0時,被該對象await的方法就會被解除凍結狀態。

方法:
await() 讓當前線程等待
countDown() 相當於cuont–

案例:

需求:請使用CountDownLatch編寫一個程序,實現以下效果:
	線程A打印:”開始計算”
	線程B:計算1--100所有數的累加和,並打印結果。
	線程A打印:”計算完畢”

//線程 A
public class CountThreadA extends Thread {
    private CountDownLatch count;

    public CountThreadA(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始計算");
        try {
            count.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "計算完畢");
    }
}

//線程 B
public class CountThreadB extends Thread {
    private CountDownLatch count;

    public CountThreadB(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }

    @Override
    public void run() {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + "結果爲: " + sum);
        count.countDown();
    }
}

//測試類
public class Work4 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(1);

        new CountThreadA("線程A",count).start();

        Thread.sleep(100); //防止線程B先執行

        new CountThreadB("線程B",count).start();

    }
}

結果:
線程A開始計算
線程B結果爲: 5050
線程A計算完畢

CyclicBarrier: 被該對象await的數量達到指定數量時,會執行指定的線程

構造:new CyclicBarrier(個數, Runnable{});
方法:await() 讓該線程等待

案例:

public class MyThread extends Thread {
    private CyclicBarrier barrier;

    public MyThread(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println("我執行完畢!進入等待狀態!");
        try {
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}


public class Demo {
    public static void main(String[] args) {
        //當有一個線程被該對象.await時觸發該線程。
        CyclicBarrier barrier = new CyclicBarrier(1, new Runnable() {
            @Override
            public void run() {
                System.out.println("已經讓一個線程等待了!");
            }
        });

        new MyThread(barrier).start();
    }
}

結果:
我執行完畢!進入等待狀態!
已經讓一個線程等待了!

(等待員工到齊開會案例!)

Semaphore: 創建對象時傳入指定的int值,表示一次最多有幾個線程併發執行,
acquire(); 表示獲取執行資格
release(); 表示釋放執行資格

請使用Semaphore編寫一個程序,實現以下效果:
有10名遊客要參觀展覽室,而“展覽室”同時只允許最多“三個遊客”參觀,每個遊客參觀時間2秒。


public class SemaThread extends Thread {

    private Semaphore semaphore;

    public SemaThread(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire(); //獲取執行資格
            System.out.println(Thread.currentThread().getName() + "正在參觀");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "完畢");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            semaphore.release(); //釋放執行資格
        }
    }
}


public class Work6 {
    public static void main(String[] args) {
    	//傳入3代表每次最多只能有3個線程併發執行
        Semaphore s = new Semaphore(3);

        for (int i = 0; i < 10; i++) {
            new SemaThread(s).start();
        }

    }
}

Exchanger:
可以讓兩個線程之間交換數據
exchange(V x);參數是傳遞給對方線程的,接收的返回值是對方線程傳過來的數據。

請使用Exchanger編寫一個程序,實現兩個線程的信息交互:
	線程A給線程B:一條娛樂新聞
	線程B給線程A:一條體育新聞


public class ExchangerThreadA extends Thread {
    private Exchanger<String> exchanger;

    public ExchangerThreadA(Exchanger<String> exchanger,String name) {
        super(name);
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        try {
            String e = exchanger.exchange(" 一條娛樂新聞");
            System.out.println(Thread.currentThread().getName() + e);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class ExchangerThreadB extends Thread {
    private Exchanger<String> exchanger;

    public ExchangerThreadB(Exchanger<String> exchanger,String name) {
        super(name);
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        try {
            String e = exchanger.exchange(" 一條體育新聞");
            System.out.println(Thread.currentThread().getName() + e);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}



public class Work7 {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger();

        new ExchangerThreadA(exchanger,"線程A").start();
        new ExchangerThreadB(exchanger,"線程B").start();
    }
}

結果:
線程A 一條體育新聞 
線程B 一條娛樂新聞

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