目錄
1.JUC并發編程的核心類
2.TimeUnit(時間單元)
3.原子操作類
4.CAS 、AQS機制
1.JUC并發編程的核心類
雖然java中的多線程有效的提升了程序的效率,但是也引發了一系列可能發生的問題,比如死鎖,公平性、資源管理以及如何面對線程安全性帶來的諸多危害。為此,java就提供了一個專門的并發編程包java.util.concurrent(簡稱JUC)。此包能夠有效的減少了競爭條件和死鎖問題。
以下介紹JUC包中核心的類
類名 | 描述 |
---|---|
Executor | Executor 是一個接口,定義了一種執行任務的方式,其目的是將任務的提交與任務的執行解耦。 |
ExecutorService | ExecutorService 是 Executor 的子接口,提供了更豐富的功能,例如線程池管理和任務提交等。 |
ScheduledExecutorService | ScheduledExecutorService 是 ExecutorService 的子接口,可以按照計劃(時間或延遲)來執行任務。 |
CompletionService | CompletionService 是一個用于異步執行任務并獲取已完成任務結果的框架。 |
Callable | Callable 是一個代表可以返回結果或拋出異常的任務的接口。它類似于 Runnable 接口,但具有返回值。 |
Future | Future 是一個可用于獲取異步計算結果的接口。 |
ReentrantLock | ReentrantLock 是一個可重入鎖,它提供了更靈活的同步控制和更高級別的功能。 |
BlockingQueue | BlockingQueue 是一個支持阻塞操作的隊列,提供了線程安全的生產者-消費者模式的實現。 |
CountDownLatch | CountDownLatch 是一個同步輔助類,允許一個或多個線程等待其他線程完成操作后再繼續執行。 |
CyclicBarrier | CyclicBarrier 是一個同步輔助類,使得一組線程能夠互相等待,直到所有線程都達到某個公共屏障點。 |
2.TimeUnit(時間單元)
這個類能夠非常好的讓我們實現各種時間之間的轉換。TimeUnit類的是枚舉類,里面有DAYS(天),HOURS(小時),MINUTES(分鐘),SECONDS(秒),MILLISECONDS(毫秒),NANNOSECONDS(納秒)
TimeUnit類中常用的方法:
方法簽名 | 描述 |
---|---|
public long convert(long sourceDuration, long srcDuration) | 該方法用于將給定的時間源持續時間轉換為目標持續時間。 |
public void sleep(long timeout) throws InterruptedException | 該方法使當前線程進入休眠狀態,暫停執行一段指定的時間(以毫秒為單位)。如果在休眠期間中斷了線程,則會拋出 InterruptedException 異常。 |
具體應用案例:
1.時間轉換與輸出一個月后的日期
package Example2101;import java.util.Date;
import java.util.concurrent.TimeUnit;public class javaDemo {public static void main(String[] args) {
// 五個小時時間long hours = 5;
// 通過SECONDS類將5個小時轉為秒long seconds = TimeUnit.SECONDS.convert(hours,TimeUnit.HOURS);System.out.println(seconds);// 獲取當前時間long now = System.currentTimeMillis();long furture = now + TimeUnit.MILLISECONDS.convert(30,TimeUnit.DAYS);System.out.println("Now Time is"+new Date(now));Date futureDay = new Date(furture);System.out.println("after mounth time is"+futureDay);}
}
?
案例2:定義一個鬧鐘,這個鬧鐘在5天后會自動發送消息
這種鬧鐘形式可以通過線程的睡眠機制進行完成,但是一般情況下如果使用線程的睡眠Thread.sleep()里面放的是毫秒,如果要睡眠五天,那么需要設置的數值會非常非常大的,所以可以使用TimeUnit類的睡眠方法實現自定義睡眠。
package Example2102;import java.util.concurrent.TimeUnit;public class javaDemo {public static void main(String[] args) {new Thread(()->{try {
// 通過TimeUnit下的Days類的sleep函數定義五天時間TimeUnit.DAYS.sleep(5);System.out.println("鬧鐘響了!!!!!!");}catch (InterruptedException e){e.printStackTrace();}},"鬧鐘").start();}
}
3.原子操作類
問題引出:一般情況下如果多線程進行競爭一個變量時候會引發數據錯亂的問題。比如多線程下售票員售票案例,由于多個線程競爭,一張票可能已經被賣出去了,但是其他的售票員并不知道,繼續售賣同一張票。在之前的時候我們通過了Sychronized()同步位解決了這個問題。但是用這個方法也有不小的弊端,那就是程序效率會大大下降。為此JUC提供了一個新的方式解決這個問題,那就是原子操作類。
首先理解原子性,原子是不可分割的最小物體,在編程中是指一種操作要么做了,要么不做。不可以中斷的一種操作。原子操作類具有更高效率,更安全,更簡單用法
原子操作類分為很多類,大致分為4類:
基本類型:AtomicInteger 、AtomicLong、AtomicBoolean
數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
引用類型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
對象屬性修改類型:
AtomicIntegerFieldUpdater;AtomicLongFiledUpdater;AtomicReferenceFieldUpdater;
?1.基本類型的原子操作類
基本類型:AtomicInteger 、AtomicLong、AtomicBoolean
基本類型之間的操作是差不多的,這里用AtomicLong舉例
AtomicLong的常用方法
方法 | 描述 |
---|---|
AtomicLong(long initValue) | 創建一個新的AtomicLong實例,并設置初始值為initValue。 |
get() | 獲取當前存儲在AtomicLong中的值。 |
set(long newValue) | 將AtomicLong的值設置為newValue。 |
getAndIncrement() | 先獲取當前存儲在AtomicLong中的值,然后將AtomicLong的值增加1。返回先前的值。 |
setAndIncrement() | 將AtomicLong的值增加1。返回增加前的值。 |
decrementAndGet() | 將AtomicLong的值減少1,并返回減少后的值。 |
使用類方法的關鍵就在于熟悉add(增加) decrement(自減)increment(自增) set(設置值) get(獲取類內部的數據) 方法就是這幾個操作之間的組合
案例代碼:多個售票員售賣100張票
package Example2103;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;public class javaDemo {public static void main(String[] args) {
// 創建原子操作類AtomicInteger ticket = new AtomicInteger(6);AtomicInteger flag = new AtomicInteger(1);//標志還有票// 創建三個線程進行售票for (int i =0;i<3;i++){new Thread(()->{while (ticket.get()>0){System.out.println("售票員"+Thread.currentThread().getName()+"售賣第"+ticket.decrementAndGet()+"張票");try {
// 設置兩秒睡眠TimeUnit.SECONDS.sleep(2);}catch (Exception e){e.printStackTrace();}
// 如果沒有票了就將標志位的值設置為0,表示沒有票了if (ticket.get() == 0){flag.set(0);System.out.println("賣完了");}}}).start();}}
}
?
?可以看到即使沒有使用同步機制也實現了同步的效果。
2.數組原子操作類
數組原子操作類有:AtomicArrayInteger AtomicLongArray AtomicReferenceArray(對象數組)
由于三者這件的使用區別不大,所以這里展示AtomicReferenceArray
AtomicReferenceArray常用方法:
方法 | 描述 |
---|---|
AtomicReferenceArray(int length) | 構造一個指定長度的AtomicReferenceArray對象。 |
AtomicReferenceArray(E[] array) | 使用給定數組初始化AtomicReferenceArray對象。 |
int length() | 返回AtomicReferenceArray的長度(即元素個數)。 |
boolean compareAndSet(int index, E expect, E update) | 將指定索引位置的元素與期望值進行比較,如果相等,則將其更新為新的值。該操作是原子性的,返回是否更新成功。 |
E get(int index) | 獲取指定索引位置的元素的值。 |
void set(int index, E newValue) | 設置指定索引位置的元素的值為newValue。 |
E getAndSet(int index, E newValue) | 獲取指定索引位置的元素的當前值,并將其設置為newValue。 |
?案例代碼:
package Example2104;import java.util.concurrent.atomic.AtomicReferenceArray;public class javaDemo {public static void main(String[] args) {String data[] = new String[]{"王二狗","180","130"};
// 初始化AtomicReferenceArray<String> array = new AtomicReferenceArray<String>(data);
// 對象數組的操作System.out.println("身高是:"+array.get(1));array.set(2,"150");System.out.println(array.get(0)+"在拼命鍛煉后體重變成:"+array.get(2));
// 篩選如果名字是王二狗的自動改名王二array.compareAndSet(0,"王二狗","王二");System.out.println("改名后名字叫"+array.get(0));}
}
?3.引用原子操作類
引用類型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
其中AtomicReference是可以直接引用數據類型的原子性操作
下面是AtomicReference的常用方法:
方法 | 描述 |
---|---|
AtomicReference() | 無參構造方法,創建一個初始值為null的AtomicReference對象。 |
V get() | 獲取當前AtomicReference對象持有的值。 |
void set(V newValue) | 設置AtomicReference對象的值為newValue。 |
boolean compareAndSet(V expect, V update) | 將AtomicReference對象的值與期望值expect進行比較(==比較),如果相等,則將其更新為新值update。該操作是原子性的,返回是否更新成功。 |
V getAndSet(V newValue) | 先獲取當前AtomicReference對象的值,然后將其設置為newValue,并返回原來的值。 |
案例代碼:使用AtomicReference進行引用操作
package Example2106;import java.util.concurrent.atomic.AtomicReference;
// 創建普通人類
class Person{private int age;private String name;private int id;Person(int age,String name,int id){this.age = age;this.name = name;this.id = id;}
}public class javaDemo {public static void main(String[] args) {Person person1 = new Person(18,"張三",001);Person person2 = new Person(20,"王思",1002);
// 傳入person1對象AtomicReference<Person> person = new AtomicReference<Person>(person1);
// 輸出對象地址System.out.println(person.get());
// 更改引用對象person.set(person2);System.out.println(person.get());}
}
AtomicStampedReference 基于版本號的數據引用。其中版本號是自己定義的int數據類型
下面是AtomicStampedReference的常用方法:
方法 | 描述 |
---|---|
AtomicStampedReference(V initRef, int initStamp) | 構造一個AtomicStampedReference對象,初始引用值為initRef,初始標記值(戳)為initStamp。 |
V getReference() | 獲取當前AtomicStampedReference對象持有的引用值。 |
void set(V newRef, int newStamp) | 設置AtomicStampedReference對象的引用值為newRef,標記值(戳)為newStamp。 |
boolean compareAndSet(V expectRef, V newRef, int expectStamp, int newStamp) | 將AtomicStampedReference對象的引用值與期望值expectRef、標記值(戳)與期望值expectStamp進行比較,如果相等,則將其更新為新值newRef和newStamp。該操作是原子性的,返回是否更新成功。 |
int attemptStamp(V expectedReference, int newStamp) | 如果當前引用值等于expectedReference,則嘗試將標記值(戳)更新為newStamp。如果更新成功,返回新的標記值(戳),否則返回當前標記值。 |
int getStamp() | 獲取當前AtomicStampedReference對象持有的標記值(戳)。 |
案例代碼:
package Example2107;import java.util.concurrent.atomic.AtomicStampedReference;class Draw{private String content = "";private String autor = "";private String title ="";Draw(String content,String autor,String title){this.content =content;this.autor = autor;this.title = title;}public void setContent(String content) {this.content = content;}public String getContent() {return content;}
}
public class javaDemo {public static void main(String[] args) {Draw draw1= new Draw("","alphaMilk","JUC并發編程原子操作類");
// 初始化內容,版本號為1AtomicStampedReference<Draw> atomicDraw = new AtomicStampedReference<Draw>(draw1,1);System.out.println(atomicDraw.getReference());
// 更新內容,版本號更改draw1.setContent("Hello,word");atomicDraw.set(draw1,2);
// 獲取當前版本System.out.println(atomicDraw.getStamp());}
}
AtomicMarkableReference與AtomicStampedReference的區別在于,一個是設置boolean類型的初始化標記,一個多設置的是int類型版本號
下面是AtomicMarkableReference的常用方法:
方法 | 描述 |
---|---|
AtomicMarkableReference(V initRef, boolean initMark) | 構造一個AtomicMarkableReference對象,初始引用值為initRef,初始標記值為initMark。 |
V getReference() | 獲取當前AtomicMarkableReference對象持有的引用值。 |
boolean isMarked() | 判斷當前AtomicMarkableReference對象是否被標記。 |
boolean compareAndSet(V expectRef, V newRef, boolean expectMark, boolean newMark) | 將AtomicMarkableReference對象的引用值與期望值expectRef、標記值與期望值expectMark進行比較,如果相等,則將其更新為新值newRef和newMark。該操作是原子性的,返回是否更新成功。 |
void set(V newRef, boolean newMark) | 設置AtomicMarkableReference對象的引用值為newRef,標記值為newMark。 |
boolean attemptMark(V expectedReference, boolean newMark) | 如果當前引用值等于expectedReference,則嘗試將標記值更新為newMark。如果更新成功,返回true,否則返回false。 |
案例代碼:
一個班統計同學是否交了班費
package Example2108;import java.util.concurrent.atomic.AtomicMarkableReference;class Student{private String name;private int id;Student(String name,int id){this.name = name;this.id = id;}
}
public class javaDemo {public static void main(String[] args) {Student stu1 = new Student("王一",001);Student stu2 = new Student("張二蛋",002);
// 王一交過班費AtomicMarkableReference<Student> atoStu = new AtomicMarkableReference<Student>(stu1,true);System.out.println(atoStu.getReference());if (atoStu.isMarked()){System.out.println("該同學交過班費");}else System.out.println("該同學尚未交過班費");
// 張二蛋沒有交班費atoStu.set(stu2,false);System.out.println(atoStu.getReference());if (atoStu.isMarked()){System.out.println("該同學交過班費");}else System.out.println("該同學尚未交過班費");}
}
4.對象屬性修改原子類
?AtomicIntegerFieldUpdater;AtomicLongFiledUpdater;AtomicReferenceFieldUpdater;
這三個類的實現原理基本差不多,所以將用AtomicIntegerFieldUpdater舉例:
以下是AtomicIntegerFieldUpdater類的常用方法:
int addAndGet(T obj, int data) | 將指定對象obj的字段值與data相加,并返回相加后的結果。 |
boolean compareAndSet(T obj, int expect, int update) | 將指定對象obj的字段值與期望值expect進行比較,如果相等,則將其更新為新值update。返回是否更新成功。 |
int get(T obj) | 獲取指定對象obj的字段值。 |
int getAndSet(T obj, int newValue) | 獲取指定對象obj的字段值,并將其設置為新值newValue。 |
int decrementAndGet(T obj) | 將指定對象obj的字段值減1,并返回減1后的結果。 |
int incrementAndGet(T obj) | 將指定對象obj的字段值加1,并返回加1后的結果。 |
?案例代碼:
package Example2109;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;class Book{volatile long id;volatile String name;Book(long id, String name){this.id = id;this.name = name;}}public class javaDemo {public static void main(String[] args) {Book book1 = new Book(114514,"Java從入門到入土");AtomicReferenceFieldUpdater<Book,String> bookmanger = AtomicReferenceFieldUpdater.newUpdater(Book.class,String.class,"name");System.out.println("更新前書本名稱為:"+bookmanger.get(book1));bookmanger.set(book1,"Java從入門到項目實戰");System.out.println("更新后書本名稱為:"+bookmanger.get(book1));}
}
?
4.CAS 、AQS機制
CAS是一條CPU并發原語。它的功能是判斷某個內存某個位置的值是否相等,如果是則改為新的值,這個操作過程屬于原子性操作。
CAS是樂觀鎖,是一種沖突重試機制,在并發競爭不是很劇烈的情況下,其操作性能會好于悲觀鎖機制(Synchronization同步處理)
*面試題為什么說 Synchronized 是一個悲觀鎖?樂觀鎖的實現原理又是什么?什么是 CAS,它有什么特性?
- Synchronized的并發策略是悲觀的,不管是否產生競爭,任何數據的操作都必須加鎖。
- 樂觀鎖的核心是CAS,CAS包括內存值、預期值、新值,只有當內存值等于預期值時,才會將內存值修改為新值。
*面試題:樂觀鎖一定就是好的嗎?
- 樂觀鎖認為對一個對象的操作不會引發沖突,所以每次操作都不進行加鎖,只是在最后提交更改時驗證是否發生沖突,如果沖突則再試一遍,直至成功為止,這個嘗試的過程稱為自旋。
- 樂觀鎖沒有加鎖,但樂觀鎖引入了ABA問題,此時一般采用版本號進行控制;
- 也可能產生自旋次數過多問題,此時并不能提高效率,反而不如直接加鎖的效率高;
- 只能保證一個對象的原子性,可以封裝成對象,再進行CAS操作;
*面試題:volatile 關鍵字的作用
對于可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。當一個共享變量被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
volatile 常用于多線程環境下的單次操作(單次讀或者單次寫)。
?