線程安全解決-鎖-併發包
爲了解決多線程安全問題,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 一條娛樂新聞