JUC

前言:

在Java中,線程部分是一個重點,本篇文章說的JUC也是關于線程的。JUC就是java.util .concurrent工具包的簡稱。這是一個處理線程的工具包,JDK 1.5開始出現的。下面一起來看看它怎么使用。


歡迎大家關注我的公眾號 javawebkf,目前正在慢慢地將簡書文章搬到公眾號,以后簡書和公眾號文章將同步更新,且簡書上的付費文章在公眾號上將免費。


一、volatile關鍵字與內存可見性

1、內存可見性:
先來看看下面的一段代碼:

public class TestVolatile {
    public static void main(String[] args){ //這個線程是用來讀取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{ //這個線程是用來修改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());
    }
}

這段代碼很簡單,就是一個ThreadDemo類繼承Runnable創建一個線程。它有一個成員變量flag為false,然后重寫run方法,在run方法里面將flag改為true,同時還有一條輸出語句。然后就是main方法主線程去讀取flag。如果flag為true,就會break掉while循環,否則就是死循環。按道理,下面那個線程將flag改為true了,主線程讀取到的應該也是true,循環應該會結束。看看運行結果:


運行結果

從圖中可以看到,該程序并沒有結束,也就是死循環。說明主線程讀取到的flag還是false,可是另一個線程明明將flag改為true了,而且打印出來了,這是什么原因呢?這就是內存可見性問題。

  • 內存可見性問題:當多個線程操作共享數據時,彼此不可見。

看下圖理解上述代碼:


圖解

要解決這個問題,可以加鎖。如下:

while (true){
        synchronized (threadDemo){
            if (threadDemo.isFlag()){
                System.out.println("主線程讀取到的flag = " + threadDemo.isFlag());
                break;
            }
        }
 }

加了鎖,就可以讓while循環每次都從主存中去讀取數據,這樣就能讀取到true了。但是一加鎖,每次只能有一個線程訪問,當一個線程持有鎖時,其他的就會阻塞,效率就非常低了。不想加鎖,又要解決內存可見性問題,那么就可以使用volatile關鍵字。

2、volatile關鍵字:

  • 用法:

volatile關鍵字:當多個線程操作共享數據時,可以保證內存中的數據可見。用這個關鍵字修飾共享數據,就會及時的把線程緩存中的數據刷新到主存中去,也可以理解為,就是直接操作主存中的數據。所以在不使用鎖的情況下,可以使用volatile。如下:

public  volatile boolean flag = false;

這樣就可以解決內存可見性問題了。

  • volatile和synchronized的區別:
    volatile不具備互斥性(當一個線程持有鎖時,其他線程進不來,這就是互斥性)。
    volatile不具備原子性。

二、原子性

1、理解原子性:
上面說到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++,然后啟動十個線程去訪問。看看結果:


運行結果

可以發現,出現了重復數據。明顯產生了多線程安全問題,或者說原子性問題。所謂原子性就是操作不可再細分,而i++操作分為讀改寫三步,如下:

int temp = i;
i = i+1;
i = temp;

所以i++明顯不是原子操作。上面10個線程進行i++時,內存圖解如下:


圖解

看到這里,好像和上面的內存可見性問題一樣。是不是加個volatile關鍵字就可以了呢?其實不是的,因為加了volatile,只是相當于所有線程都是在主存中操作數據而已,但是不具備互斥性。比如兩個線程同時讀取主存中的0,然后又同時自增,同時寫入主存,結果還是會出現重復數據。

2、原子變量:
JDK 1.5之后,Java提供了原子變量,在java.util.concurrent.atomic包下。原子變量具備如下特點:

  • 有volatile保證內存可見性。
  • 用CAS算法保證原子性。

3、CAS算法:
CAS算法是計算機硬件對并發操作共享數據的支持,CAS包含3個操作數:

  • 內存值V
  • 預估值A
  • 更新值B

當且僅當V==A時,才會把B的值賦給V,即V = B,否則不做任何操作。就上面的i++問題,CAS算法是這樣處理的:首先V是主存中的值0,然后預估值A也是0,因為此時還沒有任何操作,這時V=B,所以進行自增,同時把主存中的值變為1。如果第二個線程讀取到主存中的還是0也沒關系,因為此時預估值已經變成1,V不等于A,所以不進行任何操作。

4、使用原子變量改進i++問題:
原子變量用法和包裝類差不多,如下:

 //private int i = 0;
 AtomicInteger i = new AtomicInteger();
 public int getI(){
     return i.getAndIncrement();
 }

只改這兩處即可。

三、鎖分段機制

JDK 1.5之后,在java.util.concurrent包中提供了多種并發容器類來改進同步容器類的性能。其中最主要的就是ConcurrentHashMap。

1、ConcurrentHashMap:
ConcurrentHashMap就是一個線程安全的hash表。我們知道HashMap是線程不安全的,Hash Table加了鎖,是線程安全的,因此它效率低。HashTable加鎖就是將整個hash表鎖起來,當有多個線程訪問時,同一時間只能有一個線程訪問,并行變成串行,因此效率低。所以JDK1.5后提供了ConcurrentHashMap,它采用了鎖分段機制。

ConcurrentHashMap鎖分段

如上圖所示,ConcurrentHashMap默認分成了16個segment,每個Segment都對應一個Hash表,且都有獨立的鎖。所以這樣就可以每個線程訪問一個Segment,就可以并行訪問了,從而提高了效率。這就是鎖分段。但是,java 8 又更新了,不再采用鎖分段機制,也采用CAS算法了。

2、用法:
java.util.concurrent包還提供了設計用于多線程上下文中的 Collection 實現: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當期望許多線程訪問一個給 定 collection 時,ConcurrentHashMap 通常優于同步的 HashMap, ConcurrentSkipListMap 通常優于同步的 TreeMap。當期望的讀數和遍歷遠遠 大于列表的更新數時,CopyOnWriteArrayList 優于同步的 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個線程同時訪問
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個線程并發訪問這個集合,讀取集合數據的同時再往集合中添加數據。運行這段代碼會報錯,并發修改異常。


并發修改異常

將創建集合方式改成:

private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

這樣就不會有并發修改異常了。因為這個是寫入并復制,每次生成新的,所以如果添加操作比較多的話,開銷非常大,適合迭代操作比較多的時候使用。

四、閉鎖

java.util.concurrent包中提供了多種并發容器類來改進同步容器的性能。ContDownLatch是一個同步輔助類,在完成某些運算時,只有其他所有線程的運算全部完成,當前運算才繼續執行,這就叫閉鎖。看下面代碼:

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("耗費時間為:"+(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以內的偶數
                System.out.println(i);
            }
        }
    }
}

這段代碼就是10個線程同時去輸出5000以內的偶數,然后在主線程那里計算執行時間。其實這是計算不了那10個線程的執行時間的,因為主線程與這10個線程也是同時執行的,可能那10個線程才執行到一半,主線程就已經輸出“耗費時間為x秒”這句話了。所有要想計算這10個線程執行的時間,就得讓主線程先等待,等10個分線程都執行完了才能執行主線程。這就要用到閉鎖。看如何使用:

public class TestCountDownLatch {
    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(10);//有多少個線程這個參數就是幾
        LatchDemo ld = new LatchDemo(latch);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            new Thread(ld).start();
        }
        try {
            latch.await();//這10個線程執行完之前先等待
        } catch (InterruptedException e) {
        }
        long end = System.currentTimeMillis();
        System.out.println("耗費時間為:" + (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以內的偶數
                        System.out.println(i);
                    }
                }
            } finally {
                latch.countDown();//每執行完一個就遞減一個
            }
        }
    }
}

如上代碼,主要就是用latch.countDown()latch.await()實現閉鎖,詳細請看上面注釋即可。

五、創建線程的方式 --- 實現Callable接口

直接看代碼:

public class TestCallable {
    public static void main(String[] args){
        CallableDemo callableDemo = new CallableDemo();
        //執行callable方式,需要FutureTask實現類的支持,用來接收運算結果
        FutureTask<Integer> result = new FutureTask<>(callableDemo);
        new Thread(result).start();
        //接收線程運算結果
        try {
            Integer sum = result.get();//當上面的線程執行完后,才會打印結果。跟閉鎖一樣。所有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;
    }
}

現在Callable接口和實現Runable接口的區別就是,Callable帶泛型,其call方法有返回值。使用的時候,需要用FutureTask來接收返回值。而且它也要等到線程執行完調用get方法才會執行,也可以用于閉鎖操作。

六、Lock同步鎖

在JDK1.5之前,解決多線程安全問題有兩種方式(sychronized隱式鎖):

  • 同步代碼塊
  • 同步方法

在JDK1.5之后,出現了更加靈活的方式(Lock顯式鎖):

  • 同步鎖

Lock需要通過lock()方法上鎖,通過unlock()方法釋放鎖。為了保證鎖能釋放,所有unlock方法一般放在finally中去執行。

再來看一下賣票案例:

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));
            }
        }
    }
}

多個線程同時操作共享數據ticket,所以會出現線程安全問題。會出現同一張票賣了好幾次或者票數為負數的情況。以前用同步代碼塊和同步方法解決,現在看看用同步鎖怎么解決。

class Ticket implements Runnable {
    private Lock lock = new ReentrantLock();//創建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();//釋放鎖
            }

        }
    }
}

直接創建lock對象,然后用lock()方法上鎖,最后用unlock()方法釋放鎖即可。

七、等待喚醒機制

1、虛假喚醒問題:
生產消費模式是等待喚醒機制的一個經典案例,看下面的代碼:

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,"生產者A").start();
           new Thread(consumer,"消費者B").start();
    }
}
//店員
class Clerk{
    private int product = 0;//共享數據
    public synchronized void get(){ //進貨
        if(product >= 10){
            System.out.println("產品已滿");
        }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));
        }
    }
}
//生產者
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();
        }
    }
}
//消費者
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();
        }
    }
}

這就是生產消費模式的案例,這里沒有使用等待喚醒機制,運行結果就是即使是缺貨狀態,它也會不斷的去消費,也會一直打印“缺貨”,即使是產品已滿狀態,也會不斷地進貨。用等待喚醒機制改進:

//店員
class Clerk{
    private int product = 0;//共享數據
    public synchronized void get(){ //進貨
        if(product >= 10){
            System.out.println("產品已滿");
            try {
                this.wait();//滿了就等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println(Thread.currentThread().getName()+":"+ (++product));
            this.notifyAll();//沒滿就可以進貨
        }
    }
    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();//不缺貨就可以賣
        }
    }
}

這樣就不會出現上述問題了。沒有的時候就生產,生產滿了就通知消費,消費完了再通知生產。但是這樣還是有點問題,將上述代碼做如下改動:

if(product >= 1){ //把原來的10改成1
            System.out.println("產品已滿");
         ......
public void run() {
        try {
            Thread.sleep(200);//睡0.2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0;i<20;i++){
            clerk.sell();
        }
}

就做這兩處修改,再次運行,發現雖然結果沒問題,但是程序卻一直沒停下來。出現這種情況是因為有一個線程在等待,而另一個線程沒有執行機會了,喚醒不了這個等待的線程了,所以程序就無法結束。解決辦法就是把get和sell方法里面的else去掉,不要用else包起來。但是,即使這樣,如果再多加兩個線程,就會出現負數了。

new Thread(productor, "生產者C").start();
new Thread(consumer, "消費者D").start();

運行結果:


運行結果

一個消費者線程搶到執行權,發現product是0,就等待,這個時候,另一個消費者又搶到了執行權,product是0,還是等待,此時兩個消費者線程在同一處等待。然后當生產者生產了一個product后,就會喚醒兩個消費者,發現product是1,同時消費,結果就出現了0和-1。這就是虛假喚醒。解決辦法就是把if判斷改成while。如下:

 public synchronized void get() { //進貨
        while (product >= 1) {
            System.out.println("產品已滿");
            try {
                this.wait();//滿了就等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
            System.out.println(Thread.currentThread().getName() + ":" + (++product));
            this.notifyAll();//沒滿就可以進貨
    }
    public synchronized void sell() {//賣貨
        while (product <= 0) {//為了避免虛假喚醒問題,wait方法應該總是在循環中使用
            System.out.println("缺貨");
            try {
                this.wait();//缺貨就等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
            System.out.println(Thread.currentThread().getName() + ":" + (--product));
            this.notifyAll();//不缺貨就可以賣
    }

只需要把if改成while,每次都再去判斷一下,就可以了。

2、用Lock鎖實現等待喚醒:

class Clerk {
    private int product = 0;//共享數據
    private Lock lock = new ReentrantLock();//創建鎖對象
    private Condition condition = lock.newCondition();//獲取condition實例
    public  void get() { //進貨
        lock.lock();//上鎖
        try {
            while (product >= 1) {
                System.out.println("產品已滿");
                try {
                    condition.await();//滿了就等待
                } catch (InterruptedException e) {
                }
            }
            System.out.println(Thread.currentThread().getName() + ":" + (++product));
            condition.signalAll();//沒滿就可以進貨
        }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關鍵字了,需要創建lock對象和condition實例。condition的await()方法、signal()方法和signalAll()方法分別與wait()方法、notify()方法和notifyAll()方法對應。

3、線程按序交替:
首先來看一道題:

編寫一個程序,開啟 3 個線程,這三個線程的 ID 分別為 A、B、C,
每個線程將自己的 ID 在屏幕上打印 10 遍,要求輸出的結果必須按順序顯示。
如:ABCABCABC…… 依次遞歸

分析:

線程本來是搶占式進行的,要按序交替,所以必須實現線程通信,
那就要用到等待喚醒。可以使用同步方法,也可以用同步鎖。

編碼實現:

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;//當前正在執行的線程的標記
    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();
        }
    }
}

以上編碼就滿足需求。創建三個線程,分別調用loopA、loopB和loopC方法,這三個線程使用condition進行通信。

八、ReadWriterLock讀寫鎖

我們在讀數據的時候,可以多個線程同時讀,不會出現問題,但是寫數據的時候,如果多個線程同時寫數據,那么到底是寫入哪個線程的數據呢?所以,如果有兩個線程,寫寫/讀寫需要互斥,讀讀不需要互斥。這個時候可以用讀寫鎖。看例子:

public class TestReadWriterLock {
    public static void main(String[] args){
           ReadWriterLockDemo rw = new ReadWriterLockDemo();
           new Thread(new Runnable() {//一個線程寫
               @Override
               public void run() {
                   rw.set((int)Math.random()*101);
               }
           },"write:").start();
           for (int i = 0;i<100;i++){//100個線程讀
               Runnable runnable = () -> rw.get();
               Thread thread = new Thread(runnable);
               thread.start();
           }
    }
}

class ReadWriterLockDemo{
    private int number = 0;
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //讀(可以多個線程同時操作)
    public void get(){
        readWriteLock.readLock().lock();//上鎖
        try {
            System.out.println(Thread.currentThread().getName()+":"+number);
        }finally {
            readWriteLock.readLock().unlock();//釋放鎖
        }
    }
    //寫(一次只能有一個線程操作)
    public void set(int number){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName());
            this.number = number;
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

這個就是讀寫鎖的用法。上面的代碼實現了一個線程寫,一百個線程同時讀的操作。

九、線程池

我們使用線程時,需要new一個,用完了又要銷毀,這樣頻繁的創建銷毀也很耗資源,所以就提供了線程池。道理和連接池差不多,連接池是為了避免頻繁的創建和釋放連接,所以在連接池中就有一定數量的連接,要用時從連接池拿出,用完歸還給連接池。線程池也一樣。線程池中有一個線程隊列,里面保存著所有等待狀態的線程。下面來看一下用法:

public class TestThreadPool {
    public static void main(String[] args) {
        ThreadPoolDemo tp = new ThreadPoolDemo();
        //1.創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2.為線程池中的線程分配任務
        pool.submit(tp);
        //3.關閉線程池
        pool.shutdown();
    }
}

class ThreadPoolDemo implements Runnable {
    private int i = 0;
    @Override
    public void run() {
        while (i < 100) {
            System.out.println(Thread.currentThread().getName() + ":" + (i++));
        }
    }
}

線程池用法很簡單,分為三步。首先用工具類Executors創建線程池,然后給線程池分配任務,最后關閉線程池就行了。

總結:

以上為本文全部內容,涉及到了JUC的大部分內容。 本人也是初次接觸,如有錯誤,希望大佬指點一二!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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