前言:
在Java中,線程部分是一個(gè)重點(diǎn),本篇文章說(shuō)的JUC也是關(guān)于線程的。JUC就是java.util .concurrent工具包的簡(jiǎn)稱。這是一個(gè)處理線程的工具包,JDK 1.5開(kāi)始出現(xiàn)的。下面一起來(lái)看看它怎么使用。
歡迎大家關(guān)注我的公眾號(hào) javawebkf,目前正在慢慢地將簡(jiǎn)書(shū)文章搬到公眾號(hào),以后簡(jiǎn)書(shū)和公眾號(hào)文章將同步更新,且簡(jiǎn)書(shū)上的付費(fèi)文章在公眾號(hào)上將免費(fèi)。
一、volatile關(guān)鍵字與內(nèi)存可見(jiàn)性
1、內(nèi)存可見(jiàn)性:
先來(lái)看看下面的一段代碼:
public class TestVolatile {
public static void main(String[] args){ //這個(gè)線程是用來(lái)讀取flag的值的
ThreadDemo threadDemo = new ThreadDemo();
Thread thread = new Thread(threadDemo);
thread.start();
while (true){
if (threadDemo.isFlag()){
System.out.println("主線程讀取到的flag = " + threadDemo.isFlag());
break;
}
}
}
}
@Data
class ThreadDemo implements Runnable{ //這個(gè)線程是用來(lái)修改flag的值的
public boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("ThreadDemo線程修改后的flag = " + isFlag());
}
}
這段代碼很簡(jiǎn)單,就是一個(gè)ThreadDemo類繼承Runnable創(chuàng)建一個(gè)線程。它有一個(gè)成員變量flag為false,然后重寫run方法,在run方法里面將flag改為true,同時(shí)還有一條輸出語(yǔ)句。然后就是main方法主線程去讀取flag。如果flag為true,就會(huì)break掉while循環(huán),否則就是死循環(huán)。按道理,下面那個(gè)線程將flag改為true了,主線程讀取到的應(yīng)該也是true,循環(huán)應(yīng)該會(huì)結(jié)束??纯催\(yùn)行結(jié)果:

從圖中可以看到,該程序并沒(méi)有結(jié)束,也就是死循環(huán)。說(shuō)明主線程讀取到的flag還是false,可是另一個(gè)線程明明將flag改為true了,而且打印出來(lái)了,這是什么原因呢?這就是內(nèi)存可見(jiàn)性問(wèn)題。
- 內(nèi)存可見(jiàn)性問(wèn)題:當(dāng)多個(gè)線程操作共享數(shù)據(jù)時(shí),彼此不可見(jiàn)。
看下圖理解上述代碼:

要解決這個(gè)問(wèn)題,可以加鎖。如下:
while (true){
synchronized (threadDemo){
if (threadDemo.isFlag()){
System.out.println("主線程讀取到的flag = " + threadDemo.isFlag());
break;
}
}
}
加了鎖,就可以讓while循環(huán)每次都從主存中去讀取數(shù)據(jù),這樣就能讀取到true了。但是一加鎖,每次只能有一個(gè)線程訪問(wèn),當(dāng)一個(gè)線程持有鎖時(shí),其他的就會(huì)阻塞,效率就非常低了。不想加鎖,又要解決內(nèi)存可見(jiàn)性問(wèn)題,那么就可以使用volatile關(guān)鍵字。
2、volatile關(guān)鍵字:
- 用法:
volatile關(guān)鍵字:當(dāng)多個(gè)線程操作共享數(shù)據(jù)時(shí),可以保證內(nèi)存中的數(shù)據(jù)可見(jiàn)。用這個(gè)關(guān)鍵字修飾共享數(shù)據(jù),就會(huì)及時(shí)的把線程緩存中的數(shù)據(jù)刷新到主存中去,也可以理解為,就是直接操作主存中的數(shù)據(jù)。所以在不使用鎖的情況下,可以使用volatile。如下:
public volatile boolean flag = false;
這樣就可以解決內(nèi)存可見(jiàn)性問(wèn)題了。
- volatile和synchronized的區(qū)別:
volatile不具備互斥性(當(dāng)一個(gè)線程持有鎖時(shí),其他線程進(jìn)不來(lái),這就是互斥性)。
volatile不具備原子性。
二、原子性
1、理解原子性:
上面說(shuō)到volatile不具備原子性,那么原子性到底是什么呢?先看如下代碼:
public class TestIcon {
public static void main(String[] args){
AtomicDemo atomicDemo = new AtomicDemo();
for (int x = 0;x < 10; x++){
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
private int i = 0;
public int getI(){
return i++;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getI());
}
}
這段代碼就是在run方法里面讓i++,然后啟動(dòng)十個(gè)線程去訪問(wèn)??纯唇Y(jié)果:

可以發(fā)現(xiàn),出現(xiàn)了重復(fù)數(shù)據(jù)。明顯產(chǎn)生了多線程安全問(wèn)題,或者說(shuō)原子性問(wèn)題。所謂原子性就是操作不可再細(xì)分,而i++操作分為讀改寫三步,如下:
int temp = i;
i = i+1;
i = temp;
所以i++明顯不是原子操作。上面10個(gè)線程進(jìn)行i++時(shí),內(nèi)存圖解如下:

看到這里,好像和上面的內(nèi)存可見(jiàn)性問(wèn)題一樣。是不是加個(gè)volatile關(guān)鍵字就可以了呢?其實(shí)不是的,因?yàn)榧恿藇olatile,只是相當(dāng)于所有線程都是在主存中操作數(shù)據(jù)而已,但是不具備互斥性。比如兩個(gè)線程同時(shí)讀取主存中的0,然后又同時(shí)自增,同時(shí)寫入主存,結(jié)果還是會(huì)出現(xiàn)重復(fù)數(shù)據(jù)。
2、原子變量:
JDK 1.5之后,Java提供了原子變量,在java.util.concurrent.atomic包下。原子變量具備如下特點(diǎn):
- 有volatile保證內(nèi)存可見(jiàn)性。
- 用CAS算法保證原子性。
3、CAS算法:
CAS算法是計(jì)算機(jī)硬件對(duì)并發(fā)操作共享數(shù)據(jù)的支持,CAS包含3個(gè)操作數(shù):
- 內(nèi)存值V
- 預(yù)估值A(chǔ)
- 更新值B
當(dāng)且僅當(dāng)V==A時(shí),才會(huì)把B的值賦給V,即V = B,否則不做任何操作。就上面的i++問(wèn)題,CAS算法是這樣處理的:首先V是主存中的值0,然后預(yù)估值A(chǔ)也是0,因?yàn)榇藭r(shí)還沒(méi)有任何操作,這時(shí)V=B,所以進(jìn)行自增,同時(shí)把主存中的值變?yōu)?。如果第二個(gè)線程讀取到主存中的還是0也沒(méi)關(guān)系,因?yàn)榇藭r(shí)預(yù)估值已經(jīng)變成1,V不等于A,所以不進(jìn)行任何操作。
4、使用原子變量改進(jìn)i++問(wèn)題:
原子變量用法和包裝類差不多,如下:
//private int i = 0;
AtomicInteger i = new AtomicInteger();
public int getI(){
return i.getAndIncrement();
}
只改這兩處即可。
三、鎖分段機(jī)制
JDK 1.5之后,在java.util.concurrent包中提供了多種并發(fā)容器類來(lái)改進(jìn)同步容器類的性能。其中最主要的就是ConcurrentHashMap。
1、ConcurrentHashMap:
ConcurrentHashMap就是一個(gè)線程安全的hash表。我們知道HashMap是線程不安全的,Hash Table加了鎖,是線程安全的,因此它效率低。HashTable加鎖就是將整個(gè)hash表鎖起來(lái),當(dāng)有多個(gè)線程訪問(wèn)時(shí),同一時(shí)間只能有一個(gè)線程訪問(wèn),并行變成串行,因此效率低。所以JDK1.5后提供了ConcurrentHashMap,它采用了鎖分段機(jī)制。

如上圖所示,ConcurrentHashMap默認(rèn)分成了16個(gè)segment,每個(gè)Segment都對(duì)應(yīng)一個(gè)Hash表,且都有獨(dú)立的鎖。所以這樣就可以每個(gè)線程訪問(wèn)一個(gè)Segment,就可以并行訪問(wèn)了,從而提高了效率。這就是鎖分段。但是,java 8 又更新了,不再采用鎖分段機(jī)制,也采用CAS算法了。
2、用法:
java.util.concurrent包還提供了設(shè)計(jì)用于多線程上下文中的 Collection 實(shí)現(xiàn): ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當(dāng)期望許多線程訪問(wèn)一個(gè)給 定 collection 時(shí),ConcurrentHashMap 通常優(yōu)于同步的 HashMap, ConcurrentSkipListMap 通常優(yōu)于同步的 TreeMap。當(dāng)期望的讀數(shù)和遍歷遠(yuǎn)遠(yuǎn) 大于列表的更新數(shù)時(shí),CopyOnWriteArrayList 優(yōu)于同步的 ArrayList。下面看看部分用法:
public class TestConcurrent {
public static void main(String[] args){
ThreadDemo2 threadDemo2 = new ThreadDemo2();
for (int i=0;i<10;i++){
new Thread(threadDemo2).start();
}
}
}
//10個(gè)線程同時(shí)訪問(wèn)
class ThreadDemo2 implements Runnable{
private static List<String> list = Collections.synchronizedList(new ArrayList<>());//普通做法
static {
list.add("aaa");
list.add("bbb");
list.add("ccc");
}
@Override
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());//讀
list.add("ddd");//寫
}
}
}
10個(gè)線程并發(fā)訪問(wèn)這個(gè)集合,讀取集合數(shù)據(jù)的同時(shí)再往集合中添加數(shù)據(jù)。運(yùn)行這段代碼會(huì)報(bào)錯(cuò),并發(fā)修改異常。

將創(chuàng)建集合方式改成:
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
這樣就不會(huì)有并發(fā)修改異常了。因?yàn)檫@個(gè)是寫入并復(fù)制,每次生成新的,所以如果添加操作比較多的話,開(kāi)銷非常大,適合迭代操作比較多的時(shí)候使用。
四、閉鎖
java.util.concurrent包中提供了多種并發(fā)容器類來(lái)改進(jìn)同步容器的性能。ContDownLatch是一個(gè)同步輔助類,在完成某些運(yùn)算時(shí),只有其他所有線程的運(yùn)算全部完成,當(dāng)前運(yùn)算才繼續(xù)執(zhí)行,這就叫閉鎖??聪旅娲a:
public class TestCountDownLatch {
public static void main(String[] args){
LatchDemo ld = new LatchDemo();
long start = System.currentTimeMillis();
for (int i = 0;i<10;i++){
new Thread(ld).start();
}
long end = System.currentTimeMillis();
System.out.println("耗費(fèi)時(shí)間為:"+(end - start)+"秒");
}
}
class LatchDemo implements Runnable{
private CountDownLatch latch;
public LatchDemo(){
}
@Override
public void run() {
for (int i = 0;i<5000;i++){
if (i % 2 == 0){//50000以內(nèi)的偶數(shù)
System.out.println(i);
}
}
}
}
這段代碼就是10個(gè)線程同時(shí)去輸出5000以內(nèi)的偶數(shù),然后在主線程那里計(jì)算執(zhí)行時(shí)間。其實(shí)這是計(jì)算不了那10個(gè)線程的執(zhí)行時(shí)間的,因?yàn)橹骶€程與這10個(gè)線程也是同時(shí)執(zhí)行的,可能那10個(gè)線程才執(zhí)行到一半,主線程就已經(jīng)輸出“耗費(fèi)時(shí)間為x秒”這句話了。所有要想計(jì)算這10個(gè)線程執(zhí)行的時(shí)間,就得讓主線程先等待,等10個(gè)分線程都執(zhí)行完了才能執(zhí)行主線程。這就要用到閉鎖。看如何使用:
public class TestCountDownLatch {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);//有多少個(gè)線程這個(gè)參數(shù)就是幾
LatchDemo ld = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(ld).start();
}
try {
latch.await();//這10個(gè)線程執(zhí)行完之前先等待
} catch (InterruptedException e) {
}
long end = System.currentTimeMillis();
System.out.println("耗費(fèi)時(shí)間為:" + (end - start));
}
}
class LatchDemo implements Runnable {
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {//50000以內(nèi)的偶數(shù)
System.out.println(i);
}
}
} finally {
latch.countDown();//每執(zhí)行完一個(gè)就遞減一個(gè)
}
}
}
}
如上代碼,主要就是用latch.countDown()和latch.await()實(shí)現(xiàn)閉鎖,詳細(xì)請(qǐng)看上面注釋即可。
五、創(chuàng)建線程的方式 --- 實(shí)現(xiàn)Callable接口
直接看代碼:
public class TestCallable {
public static void main(String[] args){
CallableDemo callableDemo = new CallableDemo();
//執(zhí)行callable方式,需要FutureTask實(shí)現(xiàn)類的支持,用來(lái)接收運(yùn)算結(jié)果
FutureTask<Integer> result = new FutureTask<>(callableDemo);
new Thread(result).start();
//接收線程運(yùn)算結(jié)果
try {
Integer sum = result.get();//當(dāng)上面的線程執(zhí)行完后,才會(huì)打印結(jié)果。跟閉鎖一樣。所有futureTask也可以用于閉鎖
System.out.println(sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class CallableDemo implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0;i<=100;i++){
sum += i;
}
return sum;
}
}
現(xiàn)在Callable接口和實(shí)現(xiàn)Runable接口的區(qū)別就是,Callable帶泛型,其call方法有返回值。使用的時(shí)候,需要用FutureTask來(lái)接收返回值。而且它也要等到線程執(zhí)行完調(diào)用get方法才會(huì)執(zhí)行,也可以用于閉鎖操作。
六、Lock同步鎖
在JDK1.5之前,解決多線程安全問(wèn)題有兩種方式(sychronized隱式鎖):
- 同步代碼塊
- 同步方法
在JDK1.5之后,出現(xiàn)了更加靈活的方式(Lock顯式鎖):
- 同步鎖
Lock需要通過(guò)lock()方法上鎖,通過(guò)unlock()方法釋放鎖。為了保證鎖能釋放,所有unlock方法一般放在finally中去執(zhí)行。
再來(lái)看一下賣票案例:
public class TestLock {
public static void main(String[] args) {
Ticket td = new Ticket();
new Thread(td, "窗口1").start();
new Thread(td, "窗口2").start();
new Thread(td, "窗口3").start();
}
}
class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "完成售票,余票為:" + (--ticket));
}
}
}
}
多個(gè)線程同時(shí)操作共享數(shù)據(jù)ticket,所以會(huì)出現(xiàn)線程安全問(wèn)題。會(huì)出現(xiàn)同一張票賣了好幾次或者票數(shù)為負(fù)數(shù)的情況。以前用同步代碼塊和同步方法解決,現(xiàn)在看看用同步鎖怎么解決。
class Ticket implements Runnable {
private Lock lock = new ReentrantLock();//創(chuàng)建lock鎖
private int ticket = 100;
@Override
public void run() {
while (true) {
lock.lock();//上鎖
try {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "完成售票,余票為:" + (--ticket));
}
}finally {
lock.unlock();//釋放鎖
}
}
}
}
直接創(chuàng)建lock對(duì)象,然后用lock()方法上鎖,最后用unlock()方法釋放鎖即可。
七、等待喚醒機(jī)制
1、虛假喚醒問(wèn)題:
生產(chǎn)消費(fèi)模式是等待喚醒機(jī)制的一個(gè)經(jīng)典案例,看下面的代碼:
public class TestProductorAndconsumer {
public static void main(String[] args){
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor,"生產(chǎn)者A").start();
new Thread(consumer,"消費(fèi)者B").start();
}
}
//店員
class Clerk{
private int product = 0;//共享數(shù)據(jù)
public synchronized void get(){ //進(jìn)貨
if(product >= 10){
System.out.println("產(chǎn)品已滿");
}else {
System.out.println(Thread.currentThread().getName()+":"+ (++product));
}
}
public synchronized void sell(){//賣貨
if (product <= 0){
System.out.println("缺貨");
}else {
System.out.println(Thread.currentThread().getName()+":"+ (--product));
}
}
}
//生產(chǎn)者
class Productor implements Runnable{
private Clerk clerk;
public Productor(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0;i<20;i++){
clerk.get();
}
}
}
//消費(fèi)者
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0;i<20;i++){
clerk.sell();
}
}
}
這就是生產(chǎn)消費(fèi)模式的案例,這里沒(méi)有使用等待喚醒機(jī)制,運(yùn)行結(jié)果就是即使是缺貨狀態(tài),它也會(huì)不斷的去消費(fèi),也會(huì)一直打印“缺貨”,即使是產(chǎn)品已滿狀態(tài),也會(huì)不斷地進(jìn)貨。用等待喚醒機(jī)制改進(jìn):
//店員
class Clerk{
private int product = 0;//共享數(shù)據(jù)
public synchronized void get(){ //進(jìn)貨
if(product >= 10){
System.out.println("產(chǎn)品已滿");
try {
this.wait();//滿了就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName()+":"+ (++product));
this.notifyAll();//沒(méi)滿就可以進(jìn)貨
}
}
public synchronized void sell(){//賣貨
if (product <= 0){
System.out.println("缺貨");
try {
this.wait();//缺貨就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName()+":"+ (--product));
this.notifyAll();//不缺貨就可以賣
}
}
}
這樣就不會(huì)出現(xiàn)上述問(wèn)題了。沒(méi)有的時(shí)候就生產(chǎn),生產(chǎn)滿了就通知消費(fèi),消費(fèi)完了再通知生產(chǎn)。但是這樣還是有點(diǎn)問(wèn)題,將上述代碼做如下改動(dòng):
if(product >= 1){ //把原來(lái)的10改成1
System.out.println("產(chǎn)品已滿");
......
public void run() {
try {
Thread.sleep(200);//睡0.2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0;i<20;i++){
clerk.sell();
}
}
就做這兩處修改,再次運(yùn)行,發(fā)現(xiàn)雖然結(jié)果沒(méi)問(wèn)題,但是程序卻一直沒(méi)停下來(lái)。出現(xiàn)這種情況是因?yàn)橛幸粋€(gè)線程在等待,而另一個(gè)線程沒(méi)有執(zhí)行機(jī)會(huì)了,喚醒不了這個(gè)等待的線程了,所以程序就無(wú)法結(jié)束。解決辦法就是把get和sell方法里面的else去掉,不要用else包起來(lái)。但是,即使這樣,如果再多加兩個(gè)線程,就會(huì)出現(xiàn)負(fù)數(shù)了。
new Thread(productor, "生產(chǎn)者C").start();
new Thread(consumer, "消費(fèi)者D").start();
運(yùn)行結(jié)果:

一個(gè)消費(fèi)者線程搶到執(zhí)行權(quán),發(fā)現(xiàn)product是0,就等待,這個(gè)時(shí)候,另一個(gè)消費(fèi)者又搶到了執(zhí)行權(quán),product是0,還是等待,此時(shí)兩個(gè)消費(fèi)者線程在同一處等待。然后當(dāng)生產(chǎn)者生產(chǎn)了一個(gè)product后,就會(huì)喚醒兩個(gè)消費(fèi)者,發(fā)現(xiàn)product是1,同時(shí)消費(fèi),結(jié)果就出現(xiàn)了0和-1。這就是虛假喚醒。解決辦法就是把if判斷改成while。如下:
public synchronized void get() { //進(jìn)貨
while (product >= 1) {
System.out.println("產(chǎn)品已滿");
try {
this.wait();//滿了就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (++product));
this.notifyAll();//沒(méi)滿就可以進(jìn)貨
}
public synchronized void sell() {//賣貨
while (product <= 0) {//為了避免虛假喚醒問(wèn)題,wait方法應(yīng)該總是在循環(huán)中使用
System.out.println("缺貨");
try {
this.wait();//缺貨就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (--product));
this.notifyAll();//不缺貨就可以賣
}
只需要把if改成while,每次都再去判斷一下,就可以了。
2、用Lock鎖實(shí)現(xiàn)等待喚醒:
class Clerk {
private int product = 0;//共享數(shù)據(jù)
private Lock lock = new ReentrantLock();//創(chuàng)建鎖對(duì)象
private Condition condition = lock.newCondition();//獲取condition實(shí)例
public void get() { //進(jìn)貨
lock.lock();//上鎖
try {
while (product >= 1) {
System.out.println("產(chǎn)品已滿");
try {
condition.await();//滿了就等待
} catch (InterruptedException e) {
}
}
System.out.println(Thread.currentThread().getName() + ":" + (++product));
condition.signalAll();//沒(méi)滿就可以進(jìn)貨
}finally {
lock.unlock();//釋放鎖
}
}
public void sell() {//賣貨
lock.lock();//上鎖
try {
while (product <= 0) {
System.out.println("缺貨");
try {
condition.await();//缺貨就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (--product));
condition.signalAll();//不缺貨就可以賣
}finally {
lock.unlock();//釋放鎖
}
}
}
使用lock同步鎖,就不需要sychronized關(guān)鍵字了,需要?jiǎng)?chuàng)建lock對(duì)象和condition實(shí)例。condition的await()方法、signal()方法和signalAll()方法分別與wait()方法、notify()方法和notifyAll()方法對(duì)應(yīng)。
3、線程按序交替:
首先來(lái)看一道題:
編寫一個(gè)程序,開(kāi)啟 3 個(gè)線程,這三個(gè)線程的 ID 分別為 A、B、C,
每個(gè)線程將自己的 ID 在屏幕上打印 10 遍,要求輸出的結(jié)果必須按順序顯示。
如:ABCABCABC…… 依次遞歸
分析:
線程本來(lái)是搶占式進(jìn)行的,要按序交替,所以必須實(shí)現(xiàn)線程通信,
那就要用到等待喚醒。可以使用同步方法,也可以用同步鎖。
編碼實(shí)現(xiàn):
public class TestLoopPrint {
public static void main(String[] args) {
AlternationDemo ad = new AlternationDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopA();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopB();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopC();
}
}
}, "C").start();
}
}
class AlternationDemo {
private int number = 1;//當(dāng)前正在執(zhí)行的線程的標(biāo)記
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void loopA() {
lock.lock();
try {
if (number != 1) { //判斷
condition1.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 2;
condition2.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void loopB() {
lock.lock();
try {
if (number != 2) { //判斷
condition2.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 3;
condition3.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void loopC() {
lock.lock();
try {
if (number != 3) { //判斷
condition3.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 1;
condition1.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
以上編碼就滿足需求。創(chuàng)建三個(gè)線程,分別調(diào)用loopA、loopB和loopC方法,這三個(gè)線程使用condition進(jìn)行通信。
八、ReadWriterLock讀寫鎖
我們?cè)谧x數(shù)據(jù)的時(shí)候,可以多個(gè)線程同時(shí)讀,不會(huì)出現(xiàn)問(wèn)題,但是寫數(shù)據(jù)的時(shí)候,如果多個(gè)線程同時(shí)寫數(shù)據(jù),那么到底是寫入哪個(gè)線程的數(shù)據(jù)呢?所以,如果有兩個(gè)線程,寫寫/讀寫需要互斥,讀讀不需要互斥。這個(gè)時(shí)候可以用讀寫鎖??蠢樱?/p>
public class TestReadWriterLock {
public static void main(String[] args){
ReadWriterLockDemo rw = new ReadWriterLockDemo();
new Thread(new Runnable() {//一個(gè)線程寫
@Override
public void run() {
rw.set((int)Math.random()*101);
}
},"write:").start();
for (int i = 0;i<100;i++){//100個(gè)線程讀
Runnable runnable = () -> rw.get();
Thread thread = new Thread(runnable);
thread.start();
}
}
}
class ReadWriterLockDemo{
private int number = 0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//讀(可以多個(gè)線程同時(shí)操作)
public void get(){
readWriteLock.readLock().lock();//上鎖
try {
System.out.println(Thread.currentThread().getName()+":"+number);
}finally {
readWriteLock.readLock().unlock();//釋放鎖
}
}
//寫(一次只能有一個(gè)線程操作)
public void set(int number){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName());
this.number = number;
}finally {
readWriteLock.writeLock().unlock();
}
}
}
這個(gè)就是讀寫鎖的用法。上面的代碼實(shí)現(xiàn)了一個(gè)線程寫,一百個(gè)線程同時(shí)讀的操作。
九、線程池
我們使用線程時(shí),需要new一個(gè),用完了又要銷毀,這樣頻繁的創(chuàng)建銷毀也很耗資源,所以就提供了線程池。道理和連接池差不多,連接池是為了避免頻繁的創(chuàng)建和釋放連接,所以在連接池中就有一定數(shù)量的連接,要用時(shí)從連接池拿出,用完歸還給連接池。線程池也一樣。線程池中有一個(gè)線程隊(duì)列,里面保存著所有等待狀態(tài)的線程。下面來(lái)看一下用法:
public class TestThreadPool {
public static void main(String[] args) {
ThreadPoolDemo tp = new ThreadPoolDemo();
//1.創(chuàng)建線程池
ExecutorService pool = Executors.newFixedThreadPool(5);
//2.為線程池中的線程分配任務(wù)
pool.submit(tp);
//3.關(guān)閉線程池
pool.shutdown();
}
}
class ThreadPoolDemo implements Runnable {
private int i = 0;
@Override
public void run() {
while (i < 100) {
System.out.println(Thread.currentThread().getName() + ":" + (i++));
}
}
}
線程池用法很簡(jiǎn)單,分為三步。首先用工具類Executors創(chuàng)建線程池,然后給線程池分配任務(wù),最后關(guān)閉線程池就行了。
總結(jié):
以上為本文全部?jī)?nèi)容,涉及到了JUC的大部分內(nèi)容。 本人也是初次接觸,如有錯(cuò)誤,希望大佬指點(diǎn)一二!