面試知識點三:Java多線程


35.並行和並發有什么區別?

36.線程和進程的區別?

37.守護線程是什么?

38.創建線程有哪幾種方式?

39.說一下 runnable 和 callable 有什么區別?

40.線程有哪些狀態?

41.sleep() 和 wait() 有什么區別?

42.notify()和 notifyAll()有什么區別?

43.線程的 run()和 start()有什么區別?

44.創建線程池有哪幾種方式?

45.線程池都有哪些狀態?

46.線程池中 submit()和 execute()方法有什么區別?

47.在 java 程序中怎么保證多線程的運行安全?

48.多線程鎖的升級原理是什么?

49.什么是死鎖?

50.怎么防止死鎖?

51.ThreadLocal 是什么?有哪些使用場景?

52.說一下 synchronized 底層實現原理?

53.synchronized 和 volatile 的區別是什么?

54.synchronized 和 Lock 有什么區別?

55.synchronized 和 ReentrantLock 區別是什么?

56.說一下 atomic 的原理?

 

35.並發並行有什么區別?

  1、並發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干片,使多個進程快速交替的執行。

  

  如上圖所示,並發就是只有一個CPU資源,程序(或線程)之間要競爭得到執行機會。圖中的第一個階段,在A執行的過程中,B、C不會執行,因為這段時間內這個CPU資源被A競爭到了,同理,第二階段只有B在執行,第三階段只有C在執行。其實,並發過程中,A、B、C並不是同時進行的(微觀角度),但又是同時進行的(宏觀角度)。

  2、並行(parallellism):指在同一時刻,有多條指令在多個處理器上同時執行 
  

  如圖所示,在同一時刻,ABC都是同時執行(微觀、宏觀)

  通過多線程實現並發,並行: 
  ➤ java中的Thread類定義了多線程,通過多線程可以實現並發或並行。 
  ➤ 在CPU比較繁忙,資源不足的時候(開啟了很多進程),操作系統只為一個含有多線程的進程分配僅有的CPU資源,這些線程就會為自己盡量多搶時間片,這就是通過多線程實現並發,線程之間會競爭CPU資源爭取執行機會。 
  ➤ 在CPU資源比較充足的時候,一個進程內的多線程,可以被分配到不同的CPU資源,這就是通過多線程實現並行。 
  ➤ 至於多線程實現的是並發還是並行?上面所說,所寫多線程可能被分配到一個CPU內核中執行,也可能被分配到不同CPU執行,分配過程是操作系統所為,不可人為控制。所以,如果有人問我我所寫的多線程是並發還是並行的?我會說,都有可能。 
  ➤ 不管並發還是並行,都提高了程序對CPU資源的利用率,最大限度地利用CPU資源

36.線程和進程的區別?

  線程具有許多傳統進程所具有的特征,故又稱為輕型進程(Light—Weight Process)或進程元;而把傳統的進程稱為重型進程(Heavy—Weight Process),它相當於只有一個線程的任務。在引入了線程的操作系統中,通常一個進程都有若干個線程,至少需要一個線程。下面,我們從調度、並發性、 系統開銷、擁有資源等方面,來比較線程與進程。

  1.調度

  在傳統的操作系統中,擁有資源的基本單位和獨立調度、分派的基本單位都是進程。而在引入線程的操作系統中,則把線程作為調度和分派的基本單位。而把進程作為資源擁有的基本單位,使傳統進程的兩個屬性分開,線程便能輕裝運行,從而可顯著地提高系統的並發程度。在同一進程中,線程的切換不會引起進程的切換,在由一個進程中的線程切換到另一個進程中的線程時,將會引起進程的切換。

  2.並發性

  在引入線程的操作系統中,不僅進程之間可以並發執行,而且在一個進程中的多個線程之間,亦可並發執行,因而使操作系統具有更好的並發性,從而能更有效地使用系統資源和提高系統吞吐量。例如,在一個未引入線程的單CPU操作系統中,若僅設置一個文件服務進程,當它由於某種原因而被阻塞時,便沒有其它的文件服務進程來提供服務。在引入了線程的操作系統中,可以在一個文件服務進程中,設置多個服務線程,當第一個線程等待時,文件服務進程中的第二個線程可以繼續運行;當第二個線程阻塞時,第三個線程可以繼續執行,從而顯著地提高了文件服務的質量以及系統吞吐量。

  3.擁有資源

  不論是傳統的操作系統,還是設有線程的操作系統,進程都是擁有資源的一個獨立單位,它可以擁有自己的資源。一般地說,線程自己不擁有系統資源(也有一點必不可少的資源),但它可以訪問其隸屬進程的資源。亦即,一個進程的代碼段、數據段以及系統資源,如已打開的文件、I/O設備等,可供同一進程的其它所有線程共享。

  4.系統開銷

  由於在創建或撤消進程時,系統都要為之分配或回收資源,如內存空間、I/O設備等。因此,操作系統所付出的開銷將顯著地大於在創建或撤消線程時的開銷。類似地,在進行進程切換時,涉及到整個當前進程CPU環境的保存以及新被調度運行的進程的CPU環境的設置。而線程切換只須保存和設置少量寄存器的內容,並不涉及存儲器管理方面的操作。可見,進程切換的開銷也遠大於線程切換的開銷。此外,由於同一進程中的多個線程具有相同的地址空間,致使它們之間的同步和通信的實現,也變得比較容易。在有的系統中,線程的切換、同步和通信都無須操作系統內核的干預 。

37.守護線程是什么?

  守護線程(即daemon thread),是個服務線程,准確地來說就是服務其他的線程,這是它的作用。而其他的線程只有一種,那就是用戶線程。所以java里線程分2種,

  1、守護線程,比如垃圾回收線程,就是最典型的守護線程。
  2、用戶線程,就是應用程序里的自定義線程。
  守護線程,專門用於服務其他的線程,如果其他的線程(即用戶自定義線程)都執行完畢,連main線程也執行完畢,那么jvm就會退出(即停止運行)——此時,連jvm都停止運行了,守護線程當然也就停止執行了。再換一種說法,如果有用戶自定義線程存在的話,jvm就不會退出——此時,守護線程也不能退出,也就是它還要運行,干嘛呢,就是為了執行垃圾回收的任務啊。
  用戶也可以在應用程序代碼自定義守護線程,只需要調用Thread類的設置方法setDaemon(boolean)設置一下即可,舉例:
public class Thread01 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

  測試一下,將thread設置成守護線程:

public class test {
    public static void main(String[] args) {
        Thread thread = new Thread01();
        thread.setDaemon(true);
        thread.start();
    }
}

  結果可能是如下一種,也可能什么都不打印

  0
  1
  2
  3

  thread被設置成守護線程,那么用戶線程是main線程,當main線程執行完了之后,JVM退出,守護線程thread也就不再執行。

38.創建線程有哪幾種方式?

  三種

  1、繼承Thread類,重寫父類的run()方法

  2、實現Runnable接口,重寫run()方法,通過其實現類使用Thread

  3、實現Callable接口,重寫call()方法,通過Runnable實現類使用Thread

39.說一下 runnable 和 callable 有什么區別?

  1、Runnable沒有返回值;Callable可以返回執行結果,是個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果

  2、Callable接口的call()方法允許拋出異常;Runnable的run()方法異常只能在內部消化,不能往上繼續拋

  注:Callalbe接口支持返回執行結果,需要調用FutureTask.get()得到,此方法會阻塞主進程的繼續往下執行,如果不調用不會阻塞。

  Runnable接口:

public interface Runnable {
    public abstract void run();
}

  Callable接口:

public interface Callable<V> {
    V call() throws Exception;
}

  Java Runnable接口和Callable接口的區別

40.線程有哪些狀態?

  虛擬機中的線程狀態有六種,定義在Thread.State中:

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
}

  1、New:新建狀態

  當線程實例被new出來之后,調用start()方法之前,線程實例處於新建狀態。比如"Thread thread = new Thread()",thread就是一個處於NEW狀態的線程。

  2、Runnable:可運行狀態

  new出來線程,調用start()方法即處於Runnable狀態了。處於Runnable狀態的線程可能正在Java虛擬機中運行,也可能正在等待處理器的資源,因為一個線程必須獲得CPU的資源后,才可以運行其run()方法中的內容,否則排隊等待。

  3、Blocked:阻塞狀態

  如果某一線程正在等待監視器鎖,以便進入一個同步的塊/方法,那么這個線程的狀態就是阻塞Bloked。

  4、Waiting:等待狀態

  某一線程因為調用不帶超時的Object的wait()方法、不帶超時的Thread的join()方法、LockSupport的park()方法,就會處於等待Waiting狀態,等待被其它線程喚醒。

  5、Timed_Waiting:超時等待狀態

  某一線程因為調用帶有指定正等待時間(即傳入時間參數)的Object的wait()方法、Thread的join()方法、Thread的sleep()方法、LockSupport的parkNanos()方法、LockSupport的parkUntil()方法,就會處於超時等待Timed_Waiting狀態。

  6、Terminated:中止狀態

  線程調用終止或者run()方法執行結束后,線程即處於終止狀態。處於終止狀態的線程不具備繼續運行的能力。

  線程的轉換狀態

  上面也提到了,某一時間點線程的狀態只能是上述6個狀態中的其中一個;但是,線程在程序運行過程中的狀態是會發生變化的,由一個狀態轉變為另一個狀態,那么下面給出線程狀態轉換圖幫助我們清晰地理解線程的狀態轉變過程:

  

  Java多線程2:線程的使用及其生命周期

41.sleep() 和 wait() 有什么區別?

  1、這兩個方法來自不同的類,sleep來自Thread類,而wait來自Object類。

  2、sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。

  3、使用范圍:wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用 

42.notify()和 notifyAll()有什么區別?

  假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因為wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。

  如果另外的一個線程調用了相同對象的notifyAll()方法,那么處於該對象的等待池中的線程就會全部進入該對象的鎖池中,准備爭奪鎖的擁有權。

  如果另外的一個線程調用了相同對象的notify()方法,那么僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池.

  java中的鎖池和等待池

43.線程的 run()和 start()有什么區別?

  線程通過調用start方法啟動,然后執行run()方法中的內容,使用start方法才真正實現了多線程運行,因為這個時候不用等待我們的run方法執行完成就可以繼續執行下面的代碼,這才叫多線程嘛!

  直接使用thread執行run方法呢?因為run方法是thread里面的一個普通的方法,所以我們直接調用run方法,這個時候它是會運行在我們的主線程中的,因為這個時候我們的程序中只有主線程一個線程,那么他們的執行順序一定是順序執行,所以這樣並沒有做到多線程的這種目的。

44.創建線程池有哪幾種方式?

  1、newSingleThreadExecutor()

  創建一個單線程化的Executor,即只創建唯一的工作線程來執行任務,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO,優先級)執行。如果這個線程異常結束,會有另一個取代它,保證順序執行。單工作線程最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個線程是活動的。

  2、newFixedThreadPool(int nThreads)

  創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個工作線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閑時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會占用一定的系統資源。

  3、newCachedThreadPool()

  創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。

  這種類型的線程池特點是:

  • 工作線程的創建數量幾乎沒有限制(其實也有限制的,數目為Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
  • 如果長時間沒有往線程池中提交任務,即如果工作線程空閑了指定的時間(默認為1分鍾),則該工作線程將自動終止。終止后,如果你又提交了新的任務,則線程池重新創建一個工作線程。
  • 在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量線程同時運行,很有會造成系統癱瘓。

  4、newScheduledThreadPool(int corePoolSize)

  創建一個定長的線程池,而且支持定時的以及周期性的任務執行

  為什么要使用線程池;常見的創建線程池的4種方式

45.線程池都有哪些狀態?

  線程池的五種狀態:

  1、RUNNING

  (1) 狀態說明:線程池處在RUNNING狀態時,能夠接收新任務,以及對已添加的任務進行處理。 
  (02) 狀態切換:線程池的初始化狀態是RUNNING。換句話說,線程池被一旦被創建,就處於RUNNING狀態,並且線程池中的任務數為0!

  2、 SHUTDOWN

  (1) 狀態說明:線程池處在SHUTDOWN狀態時,不接收新任務,但能處理已添加的任務。 
  (2) 狀態切換:調用線程池的shutdown()接口時,線程池由RUNNING -> SHUTDOWN。

  3、STOP

  (1) 狀態說明:線程池處在STOP狀態時,不接收新任務,不處理已添加的任務,並且會中斷正在處理的任務。 
  (2) 狀態切換:調用線程池的shutdownNow()接口時,線程池由(RUNNING or SHUTDOWN ) -> STOP。

  4、TIDYING

  (1) 狀態說明:當所有的任務已終止,ctl記錄的”workerCount”為0,線程池會變為TIDYING狀態。當線程池變為TIDYING狀態時,會執行鈎子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變為TIDYING時,進行相應的處理;可以通過重載terminated()函數來實現。 
  (2) 狀態切換:當線程池在SHUTDOWN狀態下,阻塞隊列為空並且線程池中執行的任務也為空時,就會由 SHUTDOWN -> TIDYING。 
  當線程池在STOP狀態下,線程池中執行的任務為空時,就會由STOP -> TIDYING。

  5、 TERMINATED

  (1) 狀態說明:線程池徹底終止,就變成TERMINATED狀態。 
  (2) 狀態切換:線程池處在TIDYING狀態時,執行完terminated()之后,就會由 TIDYING -> TERMINATED。

  Java多線程11:線程池

46.線程池中 submit()和 execute()方法有什么區別?

  先看一下ExecutorService接口定義的三種submit方法

  

  1、Future<T> submit(Callable<T> task)

public class test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Future<String>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Future<String> submit = executorService.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return "call方法返回字符串";
                }
            });
            list.add(submit);
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("返回結果list的size====" + list.size());
        for (Future<String> stringFuture : list) {
            try {
                String s = stringFuture.get();
                System.out.println("返回結果====" + s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}

  結果:

返回結果list的size====5
返回結果====call方法返回字符串
返回結果====call方法返回字符串
返回結果====call方法返回字符串
返回結果====call方法返回字符串
返回結果====call方法返回字符串

  可以看到,Callable接口定義的call()方法是有返回值的,所以可以通過submit(Callable<T> task)的結果submit.get()獲取返回值。

  2、Future<?> submit(Runnable task);

public class test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Future<?>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            final int num = i;
            Future<?> submit = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(num);
                }
            });
            list.add(submit);
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("返回結果list的size====" + list.size());
        for (Future<?> future : list) {
            boolean done = future.isDone();
            System.out.println("執行是否成功===" + done);
        }
        executorService.shutdown();
    }
}

  結果:

1
2
0
4
3
返回結果list的size====5
執行是否成功===true
執行是否成功===true
執行是否成功===true
執行是否成功===true
執行是否成功===true

  可以看到,Runnable接口定義的run()方法是沒有返回值的,但是可以通過submit(Callable<T> task)的結果submit.isDone()判斷是否執行成功。

  3、Future<T> submit(Runnable task, T result)

public class test {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Future<String>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            final int num = i;
            String s = "runnable的返回值";
            Future<String> submit = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(num);
                }
            }, s);
            list.add(submit);
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("返回結果list的size====" + list.size());
        for (Future<String> future : list) {
            String s = null;
            try {
                s = future.get();
                System.out.println(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}

  結果:

0
2
4
3
1
返回結果list的size====5
runnable的返回值
runnable的返回值
runnable的返回值
runnable的返回值
runnable的返回值

  可以看到,Runnable接口定義的run()方法是沒有返回值的,但是submit()方法的第二個參數可以定義返回值。

  關於以上三種submit是怎么執行的,參考:理解三種任務Runnable和Callable和FutureTask的用法

  所以,submit()和 execute()方法有什么區別?

  1、submit()方法是否返回值的,executor()方法是沒有返回值的。

  2、submit在執行過程中與execute不一樣,不會拋出異常而是把異常保存在成員變量中,在FutureTask.get獲取的時候再把異常拋出來。

  3、execute直接拋出異常之后線程就死掉了,submit保存異常線程沒有死掉,因此execute的線程池可能會出現沒有意義的情況,因為線程沒有得到重用。而submit不會出現這種情況。

  后兩個區別參考:並發編程之submit和execute區別(七)

47.在 java 程序中怎么保證多線程的運行安全?

  線程安全在三個方面體現

  1.原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操作,(synchronized);

  synchronized是一種同步鎖,通過鎖實現原子操作。

  JDK提供鎖分兩種:一種是synchronized,依賴JVM實現鎖,因此在這個關鍵字作用對象的作用范圍內是同一時刻只能有一個線程進行操作;另一種是LOCK,是JDK提供的代碼層面的鎖,依賴CPU指令,代表性的是ReentrantLock。

  2.可見性:一個線程對主內存的修改可以及時地被其他線程看到,(volatile);

  volatile的可見性是通過內存屏障和禁止重排序實現的

  volatile會在寫操作時,會在寫操作后加一條store屏障指令,將本地內存中的共享變量值刷新到主內存。

  volatile在進行讀操作時,會在讀操作前加一條load指令,從內存中讀取共享變量。

  3.有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序,該觀察結果一般雜亂無序,(happens-before原則)。

  Java中如何保證線程安全性

48.多線程鎖的升級原理是什么?

  在所有的鎖都啟用的情況下線程進入臨界區時會先去獲取偏向鎖,如果已經存在偏向鎖了,則會嘗試獲取輕量級鎖,啟用自旋鎖,如果自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的線程阻塞掛起,直到持有鎖的線程執行完同步塊;

  偏向鎖是在無鎖爭用的情況下使用的,也就是在當前線程沒有執行完之前,沒有其它線程會執行該同步塊,一旦有了第二個線程的爭用,偏向鎖就會升級為輕量級鎖,如果輕量級鎖自旋到達閾值后,沒有獲取到鎖,就會升級為重量級鎖;

  如果線程爭用激烈,那么應該禁用偏向鎖。

49.什么是死鎖?

  簡單的說,死鎖就是線程1已經持有鎖A,要去獲取鎖B,線程2已經持有鎖B,要去獲取鎖A,即兩個線程都在等待獲取對方持有的鎖。

  圖示:

50.怎么防止死鎖?

  1、讓程序每次至多只能獲得一個鎖。當然,在多線程環境下,這種情況通常並不現實

  2、設計時考慮清楚鎖的順序,盡量減少嵌在的加鎖交互數量

  3、既然死鎖的產生是兩個線程無限等待對方持有的鎖,那么只要等待時間有個上限不就好了。當然synchronized不具備這個功能,但是我們可以使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法可以指定一個超時時限,在等待超過該時限之后便會返回一個失敗信息。

  Java多線程7:死鎖

51.ThreadLocal 是什么?有哪些使用場景?

  ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。

  ThreadLocal是如何為每個線程創建變量的副本的:

  首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。

  初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。

  然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

  最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

  Java並發編程:深入剖析ThreadLocal

52.說一下 synchronized 底層實現原理?

  從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那線程可以成功取得 monitor,並將計數器值設置為 1,取鎖成功。如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍后會分析),重入時計數器的值也會加 1。倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值為0 ,其他線程將有機會持有 monitor 。

  深入理解Java並發之synchronized實現原理

53.synchronized 和 volatile 的區別是什么?

  • volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
  • volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
  • volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
  • volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化

  synchronized和volatile的使用方法以及區別

54.synchronized 和 Lock 有什么區別?

  1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;

  2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

  4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

  5)Lock可以提高多個線程進行讀操作的效率。

  Java並發編程:Lock

55.synchronized 和 ReentrantLock 區別是什么?

  兩者的共同點:
  1)協調多線程對共享對象、變量的訪問
  2)可重入,同一線程可以多次獲得同一個鎖
  3)都保證了可見性和互斥性
  兩者的不同點:
  1)ReentrantLock顯示獲得、釋放鎖,synchronized隱式獲得釋放鎖
  2)ReentrantLock可響應中斷、可輪回,synchronized是不可以響應中斷的,為處理鎖的不可用性提供了更高的靈活性
  3)ReentrantLock是API級別的,synchronized是JVM級別的
  4)ReentrantLock可以實現公平鎖
  5)ReentrantLock通過Condition可以綁定多個條件
  6)底層實現不一樣, synchronized是同步阻塞,使用的是悲觀並發策略,lock是同步非阻塞,采用的是樂觀並發策略

56.說一下 atomic 的原理?

  以AtomicInteger為例

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}
  • 從 AtomicInteger 的內部屬性可以看出,它依賴於Unsafe 提供的一些底層能力,進行底層操作;如根據valueOffset代表的該變量值在內存中的偏移地址,從而獲取數據的。
  • 變量value用volatile修飾,保證了多線程之間的內存可見性。

  下面以getAndIncrement為例,說明其原子操作過程

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}
  • 假設線程1和線程2通過getIntVolatile拿到value的值都為1,線程1被掛起,線程2繼續執行
  • 線程2在compareAndSwapInt操作中由於預期值和內存值都為1,因此成功將內存值更新為2
  • 線程1繼續執行,在compareAndSwapInt操作中,預期值是1,而當前的內存值為2,CAS操作失敗,什么都不做,返回false
  • 線程1重新通過getIntVolatile拿到最新的value為2,再進行一次compareAndSwapInt操作,這次操作成功,內存值更新為3

  如何保證原子性:自旋 + CAS(樂觀鎖)。在這個過程中,通過compareAndSwapInt比較更新value值,如果更新失敗,重新獲取,然后更新。

  CAS是什么?

  CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。

  CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改為B,否則就什么都不做。整個比較並替換的操作是一個原子操作。

  淺談AtomicInteger實現原理

  面試必問的CAS,你懂了嗎?

  Java並發編程包中atomic的實現原理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM