線程安全:多個線程同時訪問這個類時,始終返回正常的行爲,稱爲線程安全。
1、Synchronized
1.1 多個線程一個鎖
/**同步:synchronized
* 同步的概念就是共享,我們要牢記“共享”,如果不是共享的資源就沒必要進行同步
* 異步:asynchronized
* 異步就是獨立,相互之間不受任何制約。就好像我們學習http的時候,在頁面發起ajax請求
* 我們還可以繼續瀏覽頁面或者操作頁面的其他內容,二者之間沒有任何關係。
*
* 同步的目的就是爲了線程安全,其實對於線程安全來說,需要滿足兩個特性
* 原子性(同步)
* 可見性
*/
1.1.1 示例代碼:
public class MyObject {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(4000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void method2(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 一個實例對象,他的方法加鎖了,則這個鎖是加在實例對象上的。
* ①:同一個對象在訪問被synchronized修飾的代碼塊(方法)時,需要等待鎖釋放。(public synchronized void method1(),public synchronized void method2(),)
* ②:若同一個對象訪問未被synchronized修飾的代碼塊(方法)時,無鎖。(public synchronized void method1(),public void method2())
* @param args
*/
public static void main(String[] args) {
MyObject t1 = new MyObject();
new Thread(() -> t1.method1(),"t1").start();
new Thread(() -> t1.method2(),"t2").start();
}
}
/**
* 一個實例對象,他的方法加鎖了,則這個鎖是加在實例對象上的。
* ①:同一個對象在訪問被synchronized修飾的代碼塊(方法)時,需要等待鎖釋放。(public synchronized void method1(),public synchronized void method2(),)
* ②:若同一個對象訪問未被synchronized修飾的代碼塊(方法)時,無鎖。(public synchronized void method1(),public void method2())
* @param args
*/
public static void main(String[] args) {
MyObject t1 = new MyObject();
new Thread(() -> t1.method1(),"t1").start();
new Thread(() -> t1.method2(),"t2").start();
}
}
1.1.2 實例說明:
/**
*實例總結:
* A線程先持有object對象的Lock鎖,B線程如果在這個時候調用對象中的同步
* (synchronized)方法則需要等待,也就是同步
* A線程先持有object對象的Lock鎖,B線程可以異步的方式調用對象中的非
* (synchronized)修飾的方法。
*/
1.2 多個線程多個鎖
/**
* 多個線程多個鎖:多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後,執行synchronized方法體的內容。
*/
1.2.1 示例代碼:
public class MutiThread {
/**
* 全局的屬性
*/
private static int num = 0;
public static synchronized void printNum(String tag){
try {
if (tag.equals("a")) {
num = 100;
System.out.println("tag a ,set num over ! ");
Thread.sleep(4000);
}else {
num = 200;
System.out.println("tag b, set num over !");
Thread.sleep(100);
}
System.out.println("tag "+ tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* ①:兩個線程獲取的是兩個對象的鎖,互不影響,不會有同步效果【public synchronized void printNum(String tag)】
* ②:多個對象之間如果想要共用一把鎖,需要類級別的鎖(方法被static修飾)【public static synchronized void printNum(String tag)】
* 主方法
* @param args
*/
public static void main(String[] args) {
//定義兩個不同的對象
MutiThread m1 = new MutiThread();
MutiThread m2 = new MutiThread();
new Thread(()-> m1.printNum("a")).start();
new Thread(()-> m2.printNum("b")).start();
}
}
1.2.2 執行結果:
tag a ,set num over !
tag b, set num over !
tag b, num = 200
tag a, num = 200
Process finished with exit code 0
1.2.3 實例總結:
/**
* 實例總結:
* ①:關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當作鎖。
* 所以示例代碼中那個線程先執行synchronized關鍵字的方法,那個線程就持有該方法
* 所屬對象的鎖(Lock),兩個對象,線程獲得獲得的就是兩個不同對象的鎖,他們互不影響。
* ②:有一種情況則是相同的鎖,即在靜態方法上加synchronized關鍵字,表示鎖定.class類,
* 類一級別的鎖(獨佔.class類)。
*/
1.3 synchronized 鎖對象選擇:
1.3.1 儘量不要使用字符串常量作爲鎖對象,容易出現死循環
public class StringLock {
public void method1(){
String lock = new String("就這樣吧");
synchronized ("就這樣吧")
//synchronized (lock)
{
try {
while (true) {
System.out.println("當前線程:" + Thread.currentThread().getName() + "開始");
Thread.sleep(1000);
System.out.println("當前線程:" + Thread.currentThread().getName() + "結束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
new Thread(() -> stringLock.method1(), "t1").start();
new Thread(() -> stringLock.method1(), "t2").start();
}
}
當前線程:t1開始
當前線程:t1結束
當前線程:t1開始
......
1.3.2 String作爲鎖對象,被重新賦值之後,對象引用發生變化,導致鎖變化。
**
* 字符串常量在內部發生變化,引用對象發生變化【System.out.println(lock.getBytes());】,引用的不是同一個對象,所以兩個線程同時進入了
*/
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("當前線程:"+ Thread.currentThread().getName() +"開始");
System.out.println(lock.getBytes());
lock = "change lock";
Thread.sleep(2000);
System.out.println("當前線程: "+ Thread.currentThread().getName() +"結束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChangeLock changeLock = new ChangeLock();
new Thread(() -> changeLock.method(), "t1").start();
new Thread(() -> changeLock.method(), "t2").start();
}
}
當前線程:t1開始
[B@8193e5
當前線程: t1結束
當前線程:t2開始
[B@25fef1
當前線程: t2結束
1.3.1 對象作爲鎖對象,即使對象內部屬性發生變化,則引用的是同一對象。
/**
* 對象內部屬性發生變化,則引用的是同一對象
*/
public class ModifyObject {
public String name;
public int age;
private synchronized void changeAttribute(String name ,int age){
try {
System.out.println("當前線程:"+ Thread.currentThread().getName() +"開始");
this.name = name;
this.age = age;
Thread.sleep(2000);
System.out.println("當前線程: "+ Thread.currentThread().getName() +"結束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ModifyObject modifyObject = new ModifyObject();
new Thread(() -> modifyObject.changeAttribute("張三", 18), modifyObject.toString()).start();
new Thread(() -> modifyObject.changeAttribute("李四", 25), modifyObject.toString()).start();
}
}
當前線程:com.qiulin.study.thread.day01.ModifyObject@91c18f開始
當前線程: com.qiulin.study.thread.day01.ModifyObject@91c18f結束
當前線程:com.qiulin.study.thread.day01.ModifyObject@91c18f開始
當前線程: com.qiulin.study.thread.day01.ModifyObject@91c18f結束
1.4 鎖重入:
1.4.1 方法裏面調用被synchronized修飾的方法。
/**
* synchorized鎖重入
*/
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1.....");
method2();
}
public synchronized void method2(){
System.out.println("method2.....");
method3();
}
public synchronized void method3(){
System.out.println("method3.....");
}
public static void main(String[] args) {
SyncDubbo1 t1 = new SyncDubbo1();
new Thread(() -> t1.method1()).start();
}
}
method1.....
method2.....
method3.....
1.4.2 父子類
static class Parent{
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Parent print i = "+ i);
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
static class Child extends Parent{
public synchronized void operationSub(){
try {
while (i>0){
i--;
System.out.println("Child print i =" + i);
Thread.sleep(100);
this.operationSup();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Child child = new Child();
new Thread(() -> child.operationSub()).start();
}
Child print i =2
Parent print i = 1
Child print i =0
Parent print i = -1
2. Volatile 關鍵字的主要作用是使變量在多個線程之間可見
/**
* 在java 中,每一個線程都會有一塊內存區,其中存放着所有線程共享的主內存中的變量的拷貝。
* 當線程執行時,他在自己的工作內存中操作這些變量。爲了存取一個共享的變量,
* 一個線程通常先獲取鎖並去清除它的內存工作區,把這些共享變量從所有線程的共享內存區中
* 正確的裝入到他自己的工作內存中,當線程解鎖時該工作內存中變量的值寫回到共享內存中。
*
*一個線程可以執行的操作有使用(use)、賦值(assign)、裝載(load)、存儲(store)、
* 鎖定(lock)、解鎖(unlock)。
* 而主內存可以執行的操作有讀(read)、寫(write)、鎖定(lock)、解鎖(unlock),
* 每個操作都是原子的。
*
* volatile的作用就是強制線程到主內存(共享內存)裏去讀取變量,而不去線程工作內存區裏去讀取,
* 從而實現了多個線程之間的變量可見,也就是滿足線程安全的可見性。
*/
public class VoliteRunThread {
private boolean isRunning = true;
private void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void method() {
System.out.println("進入run方法...");
try {
while (isRunning == true) {
System.out.println("線程開始工作...");
Thread.sleep(200);
}
System.out.println("線程結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
VoliteRunThread runThread = new VoliteRunThread();
new Thread(() -> runThread.method() ).start();
Thread.sleep(1000);
new Thread(() -> runThread.setRunning(false)).start();
}
}
進入run方法...
線程開始工作...
線程開始工作...
線程開始工作...
線程開始工作...
線程開始工作...
線程結束
3. 線程間通信
/**
* 線程之間的通信:線程是操作系統中獨立的個體,但這些個體如果 不經過特殊的處理就不能
* 成爲一個整體,線程間的通信就是成爲整體的必用方式之一。當線程存在通信指揮,系統間的交互性
* 會更強大,在提高CPU利用率的同時還會使開發人員對線程任務在處理的過程中進行
* 有效的把控與監督。
* <p>
* 使用wait/notify方法實現線程間的通信。(注意這兩個方法都是object的類的方法,換句話說
* java 爲所有的對象都提供這兩個方法。)
* <p>
* 1. wait 和 notify 必須配合synchronized 關鍵字使用
* 2. wait釋放鎖,notify方法不釋放鎖。
*/
3.1 volatile 變量修飾,while空輪詢檢測
public class ListAdd1 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帥");
}
public int getListSize() {
return list.size();
}
/**
* 1.線程間通信,不足:while 空輪詢,浪費資源
*
* @param args
*/
public static void main(String[] args) {
ListAdd2 obj = new ListAdd2();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + "添加一個元素...");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.listAdd();
}
}, "t1").start();
new Thread(() -> {
while (true) {
if (obj.getListSize() == 2) {
System.out.println(Thread.currentThread().getName() + "中斷了....");
throw new RuntimeException("線程中斷....");
}
}
}, "t2").start();
}
}
t1添加一個元素...
t1添加一個元素...
t1添加一個元素...
t2中斷了....
Exception in thread "t2" java.lang.RuntimeException: 線程中斷....
at com.qiulin.study.thread.day02.ListAdd1.lambda$main$1(ListAdd1.java:54)
at java.lang.Thread.run(Thread.java:745)
t1添加一個元素...
t1添加一個元素...
3.1.2 使用wait /notify 方式實現【缺點:鎖被佔據,做不到實時通信(必須等到方法執行完畢,鎖釋放後,線程2才中斷)】
/**
* 使用wait /notify 方式實現
* <p>
* 缺點:鎖被佔據,做不到實時通信【必須等到方法執行完畢,鎖釋放後,線程2才中斷】
*/
public class ListAdd2 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帥");
}
public int getListSize() {
return list.size();
}
public static void main(String[] args) {
ListAdd1 list2 = new ListAdd1();
Object lock = new Object();
new Thread(() -> {
try {
synchronized (lock) {
if (list2.getListSize() != 5) {
lock.wait();
}
System.out.println(Thread.currentThread().getName() + "中斷了....");
throw new RuntimeException("線程中斷....");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
list2.listAdd();
System.out.println(Thread.currentThread().getName() + "添加一個元素...");
Thread.sleep(100);
if (list2.getListSize() == 5) {
System.out.println("發出通知....");
lock.notify();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
}
}
3.1.3 CountDownLatch【內部以計數器控制】CountDownLatch的兩個關鍵方法 await 和 countDown 的定義,其實CountDownLatch只是簡單的利用了 AQS 的 state 屬性(表示鎖可重入的次數),CountDownLatch 的內部類 sync 重寫了 AQS 的 tryAcquireShared,CountDownLatch 的 tryAcquireShared 方法的定義是:
public int tryAcquireShared(int acquires) {
return getState() == 0? 1 : -1;
}
state的初始值就是初始化 CountDownLatch 時的計數器,在 sync 調用 AQS 的 acquireSharedInterruptibly的時候會判斷 tryAcquireShared(int acquires) 是否大於 0,如果小於 0,會將線程掛起。
/**
* 實時通信 CountDownLatch
*/
public class ListAdd3 {
private volatile List list = new ArrayList();
public void listAdd() {
list.add("我最帥");
}
public int getListSize() {
return list.size();
}
public static void main(String[] args) {
ListAdd3 list2 = new ListAdd3();
CountDownLatch downLatch = new CountDownLatch(1);//計數器減爲0時,阻塞結束
new Thread(() -> {
try {
if (list2.getListSize() != 2) {
downLatch.await();
}
System.out.println(Thread.currentThread().getName() + "中斷了....");
throw new RuntimeException("線程中斷....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
list2.listAdd();
System.out.println(Thread.currentThread().getName() + "添加一個元素...");
Thread.sleep(100);
if (list2.getListSize() == 2) {
System.out.println("發出通知....");
downLatch.countDown();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
}
}
t1添加一個元素...
t1添加一個元素...
發出通知....
t1添加一個元素...
t2中斷了....
Exception in thread "t2" java.lang.RuntimeException: 線程中斷....
at com.qiulin.study.thread.day02.ListAdd3.lambda$main$0(ListAdd3.java:31)
at java.lang.Thread.run(Thread.java:745)
t1添加一個元素...
t1添加一個元素...
4. 使用wait 和notify 實現一個阻塞隊列
/**
* 使用wait 和notify 實現一個阻塞隊列
*
* 1.需要一個承裝元素的集合
*/
public class MyQueue {
// 1.需要一個承裝元素的集合
private final LinkedList<Object> list = new LinkedList<>();
//2.需要一個計數器
private AtomicInteger count = new AtomicInteger();
//3. 需要制定上限和下限
private int minSize = 0;
//上限可以自定義指定
private int maxSize ;
//4.構造方法
public MyQueue(int size){
this.maxSize = size;
}
//5.初始化一個對象,用於加鎖
private final Object lock = new Object();
//put(將對象加入到隊列)
public void put(Object obj){
synchronized (lock){
while (maxSize == count.get()){ //當前容器已經滿了
try {
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//1.加入元素
list.add(obj);
//2.計數器累加
count.incrementAndGet();
System.out.println("新加入的元素"+obj);
//3.通知其他線程【容器滿載的情況】
lock.notify();
}
}
//get(從隊列中獲取對象)
public Object take(){
Object result = null;
synchronized (lock){
while (count.get() == this.minSize ){
try {
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//1.移除元素
result = list.removeFirst();
System.out.println("移除的元素爲:"+result);
//2.計數器遞減
count.decrementAndGet();
//3.喚醒其他線程【容器爲空的時候】
lock.notify();
}
return result;
}
public int getSize(){
return list.size();
}
public static void main(String[] args) {
MyQueue myQueue = new MyQueue(5);
myQueue.put("a");
myQueue.put("b");
myQueue.put("c");
myQueue.put("d");
myQueue.put("e");
System.out.println("當前隊列長度:"+ myQueue.getSize());
new Thread(() -> myQueue.put("f"),"t1").start();
new Thread(() ->myQueue.take(),"t2").start();
}
}
新加入的元素a
新加入的元素b
新加入的元素c
新加入的元素d
新加入的元素e
當前隊列長度:5
移除的元素爲:a
新加入的元素f
5、ThreadLocal
/**
* ThreadLocal:線程局部變量,是一種多線程併發訪問變量的解決方案。與其synchronized
* 等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用空間換時間的手段,
* 爲每個線程提供獨立副本,以保證線程安全。
*
* 從性能上說,ThreadLocal不具有絕對的優勢,在併發不是很高的情況下,加鎖的性能
* 會更好,但是作爲一套與鎖完全無關的解決方案,在併發量或者競爭激烈的額場景,使用
* ThreadLocal可以在一定程度上減少鎖競爭。
*
* 加鎖:以時間換空間
* ThreadLocal:以空間換時間
*/
public class ThreadLocalDemo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void setValue(String value){
threadLocal.set(value);
}
public String getValue(){
return threadLocal.get();
}
public static void main(String[] args) {
ThreadLocalDemo localDemo = new ThreadLocalDemo();
new Thread(() ->{
localDemo.setValue("張三");
System.out.println("當前線程"+Thread.currentThread().getName()+",ThreadLocal值爲:"+localDemo.getValue());
},"t1").start();
new Thread(() ->{
System.out.println("當前線程"+Thread.currentThread().getName()+",ThreadLocal值爲:"+localDemo.getValue());
},"t2").start();
}
}
結果:
當前線程t1,ThreadLocal值爲:張三
當前線程t2,ThreadLocal值爲:null
6、單例模式
/**
* 單例模式:最常見的額就是飢餓模式和懶漢模式
* 飢餓模式:直接實例化對象,類加載的時候創建出來
* 懶漢模式:在調方法時進行實例化對象
*
* 在多線程模式中,考慮到性能和線程安全問題,我們一般選擇下面兩種
* 比較經典的單例模式,在性能提高的同時,又保證了線程安全。
*
* ① dubble check instance 雙重空值判斷
* ② static inner class 內部類
*/
6.1 、懶漢模式
/**
* 懶漢模式:在調用方法的時候被創建出來
*/
public class LazySingleton {
private static LazySingleton instance = null;
public LazySingleton(){
}
public static LazySingleton getInstance(){
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
6.2、 餓漢模式
/**
* 餓漢模式:在類被加載時創建
*/
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
6.3、雙重空值判斷
-
/** * 雙重檢測 */ public class DubbleSingleton { private static DubbleSingleton instance; public static DubbleSingleton getInstance(){ if (instance == null){ try { //模擬初始化對象準備的時間 Thread.sleep(3000); }catch (InterruptedException e){ e.printStackTrace(); } synchronized (DubbleSingleton.class){ /* if (instance == null) { instance = new DubbleSingleton(); }*/ instance = new DubbleSingleton(); } } return instance; } public static void main(String[] args) { new Thread(()->{ System.out.println(DubbleSingleton.getInstance().hashCode()); },"t1").start(); new Thread(()->{ System.out.println(DubbleSingleton.getInstance().hashCode()); },"t2").start(); } 26103236 21414242
6.4、靜態內部類
/**
* 靜態內部類實現單例
*/
public class InnerSingleton {
private static class Singleton{
private static InnerSingleton instance = new InnerSingleton();
}
public static InnerSingleton getInstance(){
return Singleton.instance;
}
}
7、同步容器類
同步容器類都是線程安全的,但是在某些場景下可能需要加鎖來保護符合操作。複合類操作如:迭代(反覆訪問某個元素,遍歷完容器中所有的元素)、跳轉(根據指定的順序找到當前元素的下一個元素)、以及條件運算。這些複合操作在多線程併發地修改容器時,可能會表現出意外的行爲,最經典的是ConcurrentModificationException,原因是當容器迭代的過程中,被併發的修改了內容,這是由於早期迭代器設計的時候並沒有考慮併發修改的問題。
同步類容器:如Vector、HashTable。這些容器的同步功能其實都是JDK的Collections.synchronized***等工廠方法去創建實現的。其底層的機制無非就是用傳統的synchronized關鍵字對每個共用的方法都進行同步,使得每次只能有一個線程訪問容器的狀態。這很明顯不滿足高併發的需求,在保證縣城安全的同時,也必須要保證性能。
7.1、將非線程安全的容器轉換成線程安全的容器,只需要在容器外面使用Collections.sysnchronized***包裝即可。返回的容器爲同步類容器。
HashMap<Integer,String> map = new HashMap();
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
8、併發類容器
Jdk5.0之後提供了許多種併發類容器來替代同步類容器從而改善性能。同步類容器的狀態都是串行化的。他們雖然實現了線程安全,但是嚴重降低了併發性,在多線程環境下,嚴重降低了應用程序的吞吐量。
併發類容器是專門針對併發設計的,使用ConcurrentHashMap來代替HashTable,而且在ConcurrentHashMap中,添加了一些常見的複合操作的支持。以及使用了CopyOnWriteArrayList代替Vecor,併發的CopyonWriteArraySet,以及併發的Queue,ConcurrentLinkedQueue和LinkedBlockQueue,前者是高性能的隊列,後者是阻塞形式的隊列。還有ArrayBlockingQueue、PriortyQueue、SynchronousQueue等。
8.1、ConcurrentMap 接口有兩個重要的實現:
8.1.1、ConcurrentHashMap
ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的HashTable,他們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。把一個整體分成了16個段(Segment)。也就是最高支持16個線程併發修改操作。這也是在多線程場景時減小鎖的力度從而降低鎖競爭的一種方案。並且代碼中大多共享變量使用Volatile關鍵字聲明,目的是第一時間獲取修改的內容。
8.1.2、ConcurrentSkipListMap(支持併發排序功能,彌補ConcurrentHashMap),類似於TreeMap。
8.2、Copy-On-Write容器,可以在非常多的併發場景中用到。(讀多寫少場景合適,寫需要copy,耗時)
Copy-On-Write容器,其實是一種用於程序設計中的優化策略。
8.2.1、CopyOnWriteArrayList
8.2.2、CopyOnWriteArraySet
CopyOnWrite容器即寫時複製的容器。我們再往容器中添加一個元素時,不能直接往容器裏添加,而是先將容器進行Copy,複製出一個新的容器,然後新的容器再添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫在不同的容器。
-
注意:
*在多個寫(add,remove)的時候,其實方法裏面是有重入鎖保證了數據的一致性。
*
9、併發Queue
9.1、ConcurrentLinkedQueue:高性能隊列
適用於高併發場景,通過無鎖的方式,實現了高併發狀態下的高性能,通常ConcurrentLinkedQueue性能好於BlockingQueue。它是一個基於鏈接節點的無界線程安全的隊列。遵循先進先出原則。隊列不允許null元素。
9.1.1 重要方法:
add()/offer() 添加元素,在ConcurrentLinkedQueue中,這兩個方法沒有任何區別。
poll()/peek() 取出元素,poll()會隊列中刪除元素,peek()不會刪除。
9.2、BlockingQueue:阻塞隊列
9.2.1 ArrayBlockQueue
基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長的數組,以便緩存隊列中的數據對象,其內部沒實現讀寫分離,也就意味着生產和消費不能完全並行,長度是需要定義的,可以制定先進先出或者先進後出,也叫有界隊列。
9.2.2 LinkedBlockingQueue
基於鏈表的阻塞隊列,通ArrayBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),LinkedBlockingQueue之所以能夠高效的併發處理數據,是因爲其內部實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操作的完全並行運行。他是一個無界隊列。
9.2.3 SynchronousQueue
一種沒有緩衝的隊列,生產者產生的數據直接被消費者獲取並消費。SynchronousQueue 它是一個對於元素來說空了才能存入,存在才能取出的隊列,只保留一個元素在queue裏。
/** 注意1:它一種阻塞隊列,其中每個 put 必須等待一個 take,反之亦然。 同步隊列沒有任何內部容量,甚至連一個隊列的容量都沒有。 注意2:它是線程安全的,是阻塞的。 注意3:不允許使用 null 元素。 注意4:公平排序策略是指調用put的線程之間,或take的線程之間。 公平排序策略可以查考ArrayBlockingQueue中的公平策略。 注意5:SynchronousQueue的以下方法很有趣: * iterator() 永遠返回空,因爲裏面沒東西。 * peek() 永遠返回null。 * put() 往queue放進去一個element以後就一直wait直到有其他thread進來把這個element取走。 * offer() 往queue裏放一個element後立即返回,如果碰巧這個element被另一個thread取走了,offer方法返回true,認爲offer成功;否則返回false。 * offer(2000, TimeUnit.SECONDS) 往queue裏放一個element但是等待指定的時間後才返回,返回的邏輯和offer()方法一樣。 * take() 取出並且remove掉queue裏的element(認爲是在queue裏的。。。),取不到東西他會一直等。 * poll() 取出並且remove掉queue裏的element(認爲是在queue裏的。。。),只有到碰巧另外一個線程正在往queue裏offer數據或者put數據的時候,該方法纔會取到東西。否則立即返回null。 * poll(2000, TimeUnit.SECONDS) 等待指定的時間然後取出並且remove掉queue裏的element,其實就是再等其他的thread來往裏塞。 * isEmpty()永遠是true。 * remainingCapacity() 永遠是0。 * remove()和removeAll() 永遠是false。 **/
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
for (int i=0 ; i< 5; i++){
String s = UUID.randomUUID().toString();
synchronousQueue.put(s);
System.out.println("生產一個數據:" + s);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
while (true) {
try {
Thread.sleep(500);
System.out.println("消費數據:" + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
消費數據:cb02c171-775d-4389-a5d4-b24358e66dff
生產一個數據:cb02c171-775d-4389-a5d4-b24358e66dff
生產一個數據:e89fed09-a57c-4a15-9433-cd5eef0be956
消費數據:e89fed09-a57c-4a15-9433-cd5eef0be956
生產一個數據:ea6725a7-de26-4ec6-abab-2f2181a0c921
消費數據:ea6725a7-de26-4ec6-abab-2f2181a0c921
生產一個數據:35a6eef6-805f-4422-ad2f-f876f35f10c8
消費數據:35a6eef6-805f-4422-ad2f-f876f35f10c8
生產一個數據:ac74809f-647e-421a-9191-28e726311d82
消費數據:ac74809f-647e-421a-9191-28e726311d82
9.3.4 PriorityBlockingQueue
基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定,也就是說傳入的對象必須實現Comparable接口),在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖,他是一個無界隊列。
/**
*注意:隊列添加元素時,先將元素添加到數組末尾,在取出元素時,採用“上冒”的方式將該元素儘量往上冒。即:在添加的時候元素沒有排序,在取出的時候,元素按照類似冒泡的方式進行排序。
*/
public static void main(String[] args) {
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
queue.add(new Task(1, "task1"));
queue.add(new Task(3, "task3"));
queue.add(new Task(2, "task2"));
queue.add(new Task(4,"task4"));
System.out.println(queue.toString());
System.out.println("取出第一個元素:"+queue.poll());
System.out.println(queue.toString());
}
static class Task implements Comparable<Task>{
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Task(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Task task) {
return this.id>task.id?1 :(this.id == task.id ? 0 : -1);
}
@Override
public String toString() {
return "Task{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
[Task{id=1, name='task1'}, Task{id=3, name='task3'}, Task{id=2, name='task2'}, Task{id=4, name='task4'}]
取出第一個元素:Task{id=1, name='task1'}
[Task{id=2, name='task2'}, Task{id=3, name='task3'}, Task{id=4, name='task4'}]
9.3.5 DalayQueue
帶有延時時間的Queue,其中的元素只有當其指定的延時時間到了,才能夠從隊列中獲取到該元素。DelayQueue中的元素必須實現Delayed接口,DelayedQueue是一個沒有大小限制的隊列,應用場景很多,如:對緩存超時的數據進行移除、任務超時處理、空閒連接的關閉等。