JUC

前言:

在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é)果:


運(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é)果:


運(yùn)行結(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鎖分段

如上圖所示,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ā)修改異常。


并發(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é)果:


運(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)一二!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,872評(píng)論 11 349
  • JUC 原創(chuàng)者:文思,感謝尚硅谷,資料來(lái)源于尚硅谷 目錄: 1、volatile關(guān)鍵字與內(nèi)存可見(jiàn)性 2、原子變量與...
    文思li閱讀 2,538評(píng)論 0 1
  • 在一個(gè)方法內(nèi)部定義的變量都存儲(chǔ)在棧中,當(dāng)這個(gè)函數(shù)運(yùn)行結(jié)束后,其對(duì)應(yīng)的棧就會(huì)被回收,此時(shí),在其方法體中定義的變量將不...
    Y了個(gè)J閱讀 4,585評(píng)論 1 14
  • 第2章 java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理 Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令。 2.1 vo...
    kennethan閱讀 1,538評(píng)論 0 2
  • 取十盞金樽,宴好友四方。 高朋蒞臨,席開(kāi)樂(lè)張。 墨舞春秋,裙擺飛揚(yáng)。 今日對(duì)飲望山崗。 皓月當(dāng)空,舉杯滿飲豪情壯。...
    藍(lán)田飛魚(yú)閱讀 206評(píng)論 0 0

友情鏈接更多精彩內(nèi)容