自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入理解與應用多線程技術

開發(fā) 前端
如果synchronized?作用于代碼塊,反編譯可以看到兩個指令:monitorenter、monitorexit,JVM?使用monitorenter和monitorexit?兩個指令實現(xiàn)同步;如果作用synchronized?作用于方法,反編譯可以看到ACCSYNCHRONIZED?標記,JVM?通過在方法訪問標識符(flags?)中加入ACCSYNCHRONIZED來實現(xiàn)同步功能。

為什么要使用多線程

  1. 提高響應速度:對于耗時操作,使用線程可以避免阻塞主線程,提高應用程序的響應速度。
  2. 實現(xiàn)并行操作:在多CPU系統(tǒng)中,使用線程可以并行處理任務,提高CPU利用率。
  3. 改善程序結構:將一個既長又復雜的進程分為多個線程,可以使其成為幾個獨立或半獨立的運行部分,這樣有利于程序的修改和理解。
  4. 方便的通信機制:線程間可以通過共享內(nèi)存等方式進行通信,比進程間通信更方便、高效。

創(chuàng)建線程有幾種方式?

創(chuàng)建線程有四種方式:

  1. 通過繼承Thread類來創(chuàng)建線程。
  2. 通過實現(xiàn)Runnable接口來創(chuàng)建線程。
  3. 通過實現(xiàn)Callable接口來創(chuàng)建線程。
  4. 使用Executor框架來創(chuàng)建線程池。

簡單實現(xiàn)

public class ThreadTest {

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("關注公眾號:一安未來");
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("關注公眾號:一安未來");
    }
}
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThreadCallable mc = new MyThreadCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

class MyThreadCallable implements Callable {
    @Override
    public String call()throws Exception {
        return "關注公眾號:一安未來";
    }
}
public class ThreadTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Yian-Thread-pool"));
        executorOne.execute(() -> {
            System.out.println("關注公眾號:一安未來");
        });

        //關閉線程池
        executorOne.shutdown();
    }
}

線程和進程的區(qū)別

線程和進程是操作系統(tǒng)中重要的概念,都是操作系統(tǒng)資源分配的基本單位,但它們有一些關鍵的區(qū)別。

  1. 地址空間和資源擁有:進程是執(zhí)行中的一個程序,具有自己的地址空間和文件描述符等資源。線程是在進程中執(zhí)行的一個單獨的執(zhí)行路徑,共享進程的地址空間和資源。
  2. 開銷:創(chuàng)建和銷毀一個進程需要保存寄存器、棧信息以及進行資源分配和回收等操作,開銷較大。而線程的創(chuàng)建和銷毀只需保存寄存器和棧信息,開銷較小。
  3. 通信切換:進程之間必須通過IPC(進程間通信)進行通信,切換開銷相對較大。線程之間可以直接共享進程的地址空間和資源,切換開銷相對較小。
  4. 并發(fā)性:進程是獨立的執(zhí)行單元,具有自己的調(diào)度算法,在并發(fā)條件下更加穩(wěn)定可靠。而線程共享進程的資源,線程之間的調(diào)度和同步比較復雜,對并發(fā)條件的處理需要更多的注意。
  5. 一對多的關系:一個線程只能屬于一個進程,而一個進程可以擁有多個線程。

Runnable和 Callable有什么區(qū)別

  • Runnable接口只有一個需要實現(xiàn)的方法,即run()。當你啟動一個線程時,這個run()方法就會被執(zhí)行。Runnable的主要問題是它不支持返回結果
  • Callable可以返回結果,也可以拋出異常。它有一個call()方法,當調(diào)用這個方法時,這個方法就會被執(zhí)行。

volatile作用,原理

主要用于聲明變量,以指示該變量可能會被多個線程同時訪問,從而防止編譯器進行一些優(yōu)化,確保線程之間能夠正確地讀寫共享變量。volatile 提供了一種輕量級的同步機制,但它并不能替代 synchronized,因為它無法解決復合操作的原子性問題。

作用:

  • 可見性: 當一個線程修改了一個被 volatile 修飾的變量的值,其他線程能夠立即看到這個修改,即保證了變量的可見性。
  • 禁止指令重排序: volatile 修飾的變量的讀寫操作會禁止指令重排序,確保變量的寫操作不會被重排序到其它操作之前。

原理:

volatile 的實現(xiàn)原理涉及到 CPU 的緩存一致性和內(nèi)存屏障(Memory Barrier)的概念。

  • 內(nèi)存可見性: 當一個線程寫入一個 volatile 變量時,會強制將該線程對應的本地內(nèi)存中的值刷新到主內(nèi)存中,從而保證了其他線程能夠看到最新的值。同樣,當一個線程讀取一個 volatile 變量時,會強制從主內(nèi)存中讀取最新的值到本地內(nèi)存中。
  • 禁止指令重排序: volatile 修飾的變量的讀寫操作會在其前后插入內(nèi)存屏障,防止在其前后的指令被重排序。

synchronized 的實現(xiàn)原理以及鎖優(yōu)化

如果synchronized作用于代碼塊,反編譯可以看到兩個指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit兩個指令實現(xiàn)同步;如果作用synchronized作用于方法,反編譯可以看到ACCSYNCHRONIZED標記,JVM通過在方法訪問標識符(flags)中加入ACCSYNCHRONIZED來實現(xiàn)同步功能。

  • 同步代碼塊,當線程執(zhí)行到monitorenter的時候要先獲得monitor鎖,才能執(zhí)行后面的方法。當線程執(zhí)行到monitorexit的時候則要釋放鎖。
  • 同步方法,當線程執(zhí)行有ACCSYNCHRONI標志的方法,需要獲得monitor鎖。每個對象都與一個monitor相關聯(lián),線程可以占有或者釋放monitor。

monitor監(jiān)視器

操作系統(tǒng)的管程(monitors)是概念原理,ObjectMonitor是它的原理實現(xiàn)。

圖片圖片

在Java虛擬機(HotSpot)中,Monitor(管程)是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結構如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄個數(shù)
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;  // 處于wait狀態(tài)的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 處于等待鎖block狀態(tài)的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

Java Monitor 的工作機理

圖片圖片

  • 要獲取monitor的線程,首先會進入EntryList隊列。
  • 當某個線程獲取到對象的monitor后,進入Owner區(qū)域,設置為當前線程,同時計數(shù)器count加1。
  • 如果線程調(diào)用了wait()方法,則會進入WaitSet隊列阻塞等待。它會釋放monitor鎖,即將owner賦值為null,count自減1。
  • 如果其他線程調(diào)用 notify()/notifyAll() ,會喚醒WaitSet中的某個或全部線程,該線程再次嘗試獲取monitor鎖,成功即進入Owner區(qū)域。
  • 同步方法執(zhí)行完畢了,線程退出臨界區(qū),會將monitor的owner設為null,并釋放監(jiān)視鎖

對象與monitor關聯(lián)

圖片圖片

  • 在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header),實例數(shù)據(jù)(Instance Data)和對象填充(Padding)。
  • 對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段)、Class Pointer(類型指針)。

Mark Word 是用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程 ID、偏向時間戳等。

圖片圖片

重量級鎖,指向互斥量的指針。其實synchronized是重量級鎖,也就是說Synchronized的對象鎖,Mark Word鎖標識位為10,其中指針指向的是Monitor對象的起始地址。

在JDK1.6之前,synchronized的實現(xiàn)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。從JDK6開始,HotSpot虛擬機開發(fā)團隊對Java中的鎖進行優(yōu)化,如增加了適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等優(yōu)化策略,提升了synchronized的性能。

  • 偏向鎖:在無競爭的情況下,只是在Mark Word里存儲當前線程指針,CAS操作都不做。
  • 輕量級鎖:在沒有多線程競爭時,相對重量級鎖,減少操作系統(tǒng)互斥量帶來的性能消耗。但是,如果存在鎖競爭,除了互斥量本身開銷,還額外有CAS操作的開銷。
  • 自旋鎖:減少不必要的CPU上下文切換。在輕量級鎖升級為重量級鎖時,就使用了自旋加鎖的方式
  • 鎖粗化:將多個連續(xù)的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。
  • 鎖消除:虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。

線程有哪些狀態(tài)

圖片圖片

  • New:線程對象創(chuàng)建之后、但還沒有調(diào)用start()方法,就是這個狀態(tài)。
  • Runnable:它包括就緒(ready)和運行中(running)兩種狀態(tài)。如果調(diào)用start方法,線程就會進入Runnable狀態(tài)。它表示我這個線程可以被執(zhí)行啦(此時相當于ready狀態(tài)),如果這個線程被調(diào)度器分配了CPU時間,那么就可以被執(zhí)行(此時處于running狀態(tài))。
  • Blocked:阻塞的(被同步鎖或者IO鎖阻塞)。表示線程阻塞于鎖,線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(等待獲取鎖)時的狀態(tài)。比如前面有一個臨界區(qū)的代碼需要執(zhí)行,那么線程就需要等待,它就會進入這個狀態(tài)。它一般是從RUNNABLE狀態(tài)轉(zhuǎn)化過來的。如果線程獲取到鎖,它將變成RUNNABLE狀態(tài)。
  • WAITING : 永久等待狀態(tài),進入該狀態(tài)的線程需要等待其他線程做出一些特定動作(比如通知)。處于該狀態(tài)的線程不會被分配CPU執(zhí)行時間,它們要等待被顯式地喚醒,否則會處于無限期等待的狀態(tài)。一般Object.wait。
  • TIMED_WATING: 等待指定的時間重新被喚醒的狀態(tài)。有一個計時器在里面計算的,最常見就是使用Thread.sleep方法觸發(fā),觸發(fā)后,線程就進入了Timed_waiting狀態(tài),隨后會由計時器觸發(fā),再進入Runnable狀態(tài)。
  • 終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完成。

CountDownLatch與CyclicBarrier 區(qū)別

CountDownLatch和CyclicBarrier都用于讓線程等待,達到一定條件時再運行。主要區(qū)別是:

  • CountDownLatch:一個或者多個線程,等待其他多個線程完成某件事情之后才能執(zhí)行;
  • CyclicBarrier:多個線程互相等待,直到到達同一個同步點,再繼續(xù)一起執(zhí)行。

圖片圖片

多線程環(huán)境下的偽共享

CPU的緩存是以緩存行(cache line)為單位進行緩存的,當多個線程修改相互獨立的變量,而這些變量又處于同一個緩存行時就會影響彼此的性能。這就是偽共享

現(xiàn)代計算機計算模型:

圖片圖片

  • CPU執(zhí)行速度比內(nèi)存速度快好幾個數(shù)量級,為了提高執(zhí)行效率,現(xiàn)代計算機模型演變出CPU、緩存(L1,L2,L3),內(nèi)存的模型。
  • CPU執(zhí)行運算時,如先從L1緩存查詢數(shù)據(jù),找不到再去L2緩存找,依次類推,直到在內(nèi)存獲取到數(shù)據(jù)。
  • 為了避免頻繁從內(nèi)存獲取數(shù)據(jù),聰明的科學家設計出緩存行,緩存行大小為64字節(jié)。

也正是因為緩存行的存在,就導致了偽共享問題,如圖所示:

圖片圖片

假設數(shù)據(jù)a、b被加載到同一個緩存行。

  • 當線程1修改了a的值,這時候CPU1就會通知其他CPU核,當前緩存行(Cache line)已經(jīng)失效。
  • 這時候,如果線程2發(fā)起修改b,因為緩存行已經(jīng)失效了,所以「core2 這時會重新從主內(nèi)存中讀取該 Cache line 數(shù)據(jù)」。讀完后,因為它要修改b的值,那么CPU2就通知其他CPU核,當前緩存行(Cache line)又已經(jīng)失效。
  • 所以,如果同一個Cache line的內(nèi)容被多個線程讀寫,就很容易產(chǎn)生相互競爭,頻繁回寫主內(nèi)存,會大大降低性能。

解決偽共享問題的一種方法是通過填充(Padding)來確保共享的變量獨立存儲于不同的緩存行中。填充的思想是在變量之間插入一些無關的數(shù)據(jù),使它們分布到不同的緩存行,從而避免多個變量共享同一個緩存行。

在Java中,可以使用@Contended注解來避免偽共享。這個注解可以在字段上使用,它會在字段的前后插入填充,使得字段單獨占據(jù)一個緩存行。

Fork/Join框架

Fork/Join框架是Java7提供的一個用于并行執(zhí)行任務的框架,是一個把大任務分割成若干個小任務,最終匯總每個小任務結果后得到大任務結果的框架。

Fork/Join框架需要理解兩個點,「分而治之」和「工作竊取」。

分而治之

圖片圖片

工作竊取

圖片圖片

一般就是指做得快的線程(盜竊線程)搶慢的線程的任務來做,同時為了減少鎖競爭,通常使用雙端隊列,即快線程和慢線程各在一端。

ThreadLocal原理

ThreadLocal的內(nèi)存結構圖:

圖片圖片

  • Thread線程類有一個類型為ThreadLocal.ThreadLocalMap的實例變量threadLocals,即每個線程都有一個屬于自己的ThreadLocalMap。
  • ThreadLocalMap內(nèi)部維護著Entry數(shù)組,每個Entry代表一個完整的對象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
  • 并發(fā)多線程場景下,每個線程Thread,在往ThreadLocal里設置值的時候,都是往自己的ThreadLocalMap里存,讀也是以某個ThreadLocal作為引用,在自己的map里找對應的key,從而可以實現(xiàn)了線程隔離。

內(nèi)存泄露問題:指程序中動態(tài)分配的堆內(nèi)存由于某種原因沒有被釋放或者無法釋放,造成系統(tǒng)內(nèi)存的浪費,導致程序運行速度減慢或者系統(tǒng)奔潰等嚴重后果。內(nèi)存泄露堆積將會導致內(nèi)存溢出。

ThreadLocal的內(nèi)存泄露問題一般考慮和Entry對象有關,ThreadLocal::Entry被弱引用所修飾。JVM會將弱引用修飾的對象在下次垃圾回收中清除掉。這樣就可以實現(xiàn)ThreadLocal的生命周期和線程的生命周期解綁。但實際上并不是使用了弱引用就會發(fā)生內(nèi)存泄露問題,考慮下面幾個過程:

圖片圖片

當ThreadLocal Ref被回收了,由于在Entry使用的是強引用,在Current Thread還存在的情況下就存在著到達Entry的引用鏈,無法清除掉ThreadLocal的內(nèi)容,同時Entry的value也同樣會被保留;也就是說就算使用了強引用仍然會出現(xiàn)內(nèi)存泄露問題。

圖片圖片

當ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的時候就會將ThreadLocal對象清除,這個時候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref這個強引用,因此Entry中value的值任然無法清除。還是存在內(nèi)存泄露的問題。

AQS實現(xiàn)原理

AbstractQueuedSynchronizer(AQS)是Java中用于構建同步器的基礎框架。它提供了一個靈活的、可重用的同步器實現(xiàn),可以用來構建各種同步工具,如ReentrantLock、Semaphore、CountDownLatch等。AQS的核心思想是基于FIFO等待隊列,通過狀態(tài)(state)來管理線程的同步。

核心原理:

  • State(狀態(tài)): AQS 的同步狀態(tài)是一個整數(shù),表示被同步的資源的狀態(tài)。不同的同步器會使用不同的方式來表示狀態(tài)的含義,例如,ReentrantLock 使用 state 表示持有鎖的線程的數(shù)量,Semaphore 使用 state 表示可用的許可數(shù)量等。
  • FIFO 等待隊列: AQS 使用一個FIFO的等待隊列來管理獲取同步資源失敗的線程。每個節(jié)點(Node)表示一個等待線程,節(jié)點中保存了等待狀態(tài)、前驅(qū)節(jié)點、后繼節(jié)點等信息。當一個線程嘗試獲取鎖但失敗時,它會被包裝成一個節(jié)點并加入到等待隊列中。
  • 獨占模式和共享模式: AQS 支持獨占模式和共享模式。獨占模式表示只有一個線程能夠獲取同步資源,如ReentrantLock 就是獨占模式的同步器。共享模式表示多個線程可以同時獲取同步資源,如Semaphore 就是共享模式的同步器。AQS 使用 acquire 和 release 方法來分別表示獲取和釋放同步資源。
  • acquire 方法: 當線程嘗試獲取同步資源時,它會調(diào)用 AQS 的 acquire 方法。acquire 方法會根據(jù)同步狀態(tài)的不同情況進行處理,如果同步狀態(tài)允許當前線程獲取資源,則直接返回;否則,當前線程會被包裝成節(jié)點并加入到等待隊列中,然后進入自旋等待狀態(tài),直到獲取到資源。
  • release 方法: 當線程釋放同步資源時,它會調(diào)用 AQS 的 release 方法。release 方法會根據(jù)同步狀態(tài)的不同情況進行處理,然后喚醒等待隊列中的下一個線程,使其有機會獲取資源。
  • 獨占鎖和共享鎖的實現(xiàn): AQS 提供了獨占鎖的實現(xiàn)方法 tryAcquire 和 tryRelease,以及共享鎖的實現(xiàn)方法 tryAcquireShared 和 tryReleaseShared。

ReentrantLock 解析:

圖片圖片

圖片圖片

上下文切換

CPU上下文:CPU 寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計數(shù)器,則是用來存儲 CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環(huán)境,因此叫做

CPU上下文切換:把前一個任務的CPU上下文(也就是CPU寄存器和程序計數(shù)器)保存起來,然后加載新任務的上下文到這些寄存器和程序計數(shù)器,最后再跳轉(zhuǎn)到程序計數(shù)器所指的新位置,運行新任務。

圖片圖片

  • 分時調(diào)度:讓所有的線程輪流獲得CPU的使用權,并且平均分配每個線程占用的 CPU 的時間片。
  • 搶占式調(diào)度:優(yōu)先讓可運行池中優(yōu)先級高的線程占用CPU,如果可運行池中的線程優(yōu)先級相同,那么就隨機選擇一個線程,使其占用CPU。處于運行狀態(tài)的線程會一直運行,直至它不得不放棄 CPU。
責任編輯:武曉燕 來源: 一安未來
相關推薦

2024-05-17 12:56:09

C#編程線程

2023-11-13 16:33:46

2017-12-18 16:33:55

多線程對象模型

2018-03-14 15:20:05

Java多線程勘誤

2024-03-12 00:00:00

Sora技術數(shù)據(jù)

2024-04-15 00:00:00

技術Attention架構

2016-11-15 14:33:05

Flink大數(shù)據(jù)

2018-07-26 20:10:02

編程語言Java多線程

2024-06-06 09:58:13

2024-06-28 10:25:18

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結構hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2016-11-22 17:05:54

Apache Flin大數(shù)據(jù)Flink

2024-11-05 09:11:09

TypeScript開發(fā)者代碼

2024-05-10 08:18:16

分布式數(shù)據(jù)庫

2023-12-31 12:56:02

C++內(nèi)存編程

2018-05-16 11:05:49

ApacheFlink數(shù)據(jù)流

2021-10-26 17:52:52

Android插件化技術

2010-07-26 11:27:58

Perl閉包
點贊
收藏

51CTO技術棧公眾號