從 電商系統(tǒng) 角度全方位 研究+吃透 “Java多線程”(上篇)

01 前言

本章節(jié)主要分享下菊碟,多線程并發(fā)在電商系統(tǒng)下的應(yīng)用。主要從以下幾個(gè)方面深入:線程相關(guān)的基礎(chǔ)理論和工具、多線程程序下的性能調(diào)優(yōu)和電商場景下多線程的使用手蝎。


image.png

02 多線程

2.1 JU·C線程池

(1)概念

回顧線程創(chuàng)建的方式

  • 繼承Thread
  • 實(shí)現(xiàn)Runnable
  • 使用FutureTask

線程狀態(tài)

NEW:剛剛創(chuàng)建,沒做任何操作

RUNNABLE:調(diào)用run蝉娜,可以執(zhí)行丙者,但不代表一定在執(zhí)行(RUNNING,READY)

WATING:使用了waite(),join()等方法

TIMED_WATING:使用了sleep(long),wait(long),join(long)等方法

BLOCKED:搶不到鎖

TERMINATED:終止

線程池基本概念

根據(jù)上面的狀態(tài),普通線程執(zhí)行完悬秉,就會(huì)進(jìn)入TERMINA TED銷毀掉榛斯,而線程池就是創(chuàng)建一個(gè)緩沖池存放線程,執(zhí)行結(jié)束以后搂捧,該線程并不會(huì)死亡驮俗,而是再次返回線程池中成為空閑狀態(tài),等候下次任務(wù)來臨允跑,這使得線程池比手動(dòng)創(chuàng)建線程有著更多的優(yōu)勢:

  • 降低系統(tǒng)資源消耗王凑,通過重用已存在的線程,降低線程創(chuàng)建和銷毀造成的消耗聋丝;
  • 提高系統(tǒng)響應(yīng)速度索烹,當(dāng)有任務(wù)到達(dá)時(shí),通過復(fù)用已存在的線程弱睦,無需等待新線程的創(chuàng)建便能立即執(zhí)行百姓;
  • 方便線程并發(fā)數(shù)的管控。因?yàn)榫€程若是無限制的創(chuàng)建况木,可能會(huì)導(dǎo)致內(nèi)存占用過多而產(chǎn)生OOM
  • 節(jié)省cpu切換線程的時(shí)間成本(需要保持當(dāng)前執(zhí)行線程的現(xiàn)場垒拢,并恢復(fù)要執(zhí)行線程的現(xiàn)場)。
  • 提供更強(qiáng)大的功能火惊,延時(shí)定時(shí)線程池求类。(Timer vs ScheduledThreadPoolExecutor)

常用線程池類結(jié)構(gòu)

image.png

說明:

  • 最常用的是ThreadPoolExecutor
  • 調(diào)度用的ScheduledThreadPoolExecutor
  • Executors是工具類,協(xié)助創(chuàng)建線程池

(2)工作機(jī)制

在線程池的編程模式下,任務(wù)是提交給整個(gè)線程池屹耐,而不是直接提交給某個(gè)線程尸疆,線程池在拿到任務(wù)后,就在內(nèi)部尋找是否有空閑的線程,如果有寿弱,則將任務(wù)交給某個(gè)空閑的線程犯眠。一個(gè)線程同時(shí)只能執(zhí)行一個(gè)任務(wù),但可以同時(shí)向一個(gè)線程池提交多個(gè)任務(wù)症革。

線程池狀態(tài)

  • RUNNING:初始化狀態(tài)是RUNNING阔逼。線程池被一旦被創(chuàng)建,就處于RUNNING狀態(tài)地沮,并且線程池中的任務(wù)數(shù)為0嗜浮。RUNNING狀態(tài)下,能夠接收新任務(wù)摩疑,以及對(duì)已添加的任務(wù)進(jìn)行處理危融。
  • SHUTDOWN:SHUTDOWN狀態(tài)時(shí),不接收新任務(wù)雷袋,但能處理已添加的任務(wù)吉殃。調(diào)用線程池的shutdown()接口時(shí),線程池由RUNNING -> SHUTDOWN楷怒。
//shutdown后不接受新任務(wù)蛋勺,但是task1,仍然可以執(zhí)行完成
ExecutorService poolExecutor = Executors.newFixedThreadPool(5);
poolExecutor.execute(new Runnable() {
   public void run() {
       try {
           Thread.sleep(1000);
           System.out.println("finish task 1");
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
});
poolExecutor.shutdown();
poolExecutor.execute(new Runnable() {
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
});
System.out.println("ok");
  • STOP:不接收新任務(wù)鸠删,不處理已添加的任務(wù)抱完,并且會(huì)中斷正在處理的任務(wù)。調(diào)用線程池的shutdownNow()接口時(shí)刃泡,線程池由(RUNNING 或SHUTDOWN ) -> STOP
//改為shutdownNow后巧娱,任務(wù)立馬終止,sleep被打斷烘贴,新任務(wù)無法提交禁添,task1停止
poolExecutor.shutdownNow();
  • TIDYING:所有的任務(wù)已終止,ctl記錄的”任務(wù)數(shù)量”為0桨踪,線程池會(huì)變?yōu)門IDYING老翘。線程池變?yōu)門IDYING狀態(tài)時(shí),會(huì)執(zhí)行鉤子函數(shù)terminated()锻离,可以通過重載terminated()函數(shù)來實(shí)現(xiàn)自定義行為
//自定義類铺峭,重寫terminated方法
public class MyExecutorService extends ThreadPoolExecutor {
   public MyExecutorService(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
       super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue);
   }
   @Override
   protected void terminated() {
       super.terminated();
       System.out.println("treminated");
   }

   //調(diào)用 shutdownNow, ternimated方法被調(diào)用打印
   public static void main(String[] args) throws InterruptedException {
       MyExecutorService service = new
MyExecutorService(1,2,10000,TimeUnit.SECONDS,new
LinkedBlockingQueue<Runnable>(5));
       service.shutdownNow();
   }
}
  • TERMINA TED:線程池處在TIDYING狀態(tài)時(shí)纳账,執(zhí)行完terminated()之后逛薇,就會(huì)由TIDYING ->TERMINA TED

(3)結(jié)構(gòu)說明

image.png

整理:最強(qiáng)“高并發(fā)”系統(tǒng)設(shè)計(jì) 46 連問,分分鐘秒殺一眾面試者

(4)任務(wù)提交流程

  1. 添加任務(wù)疏虫,如果線程池中的線程數(shù)沒有達(dá)到coreSize,會(huì)創(chuàng)建線程執(zhí)行任務(wù)
  2. 當(dāng)達(dá)到coreSize,把任務(wù)放workQueue中
  3. 當(dāng)queue滿了,未達(dá)maxsize創(chuàng)建心線程
  4. 線程數(shù)也達(dá)到maxsize,再添加任務(wù)會(huì)執(zhí)行reject策略
  5. 任務(wù)執(zhí)行完畢,超過keepactivetime,釋放超時(shí)的非核心線程,最終恢復(fù)到coresize大小

(5)源碼剖析

execute方法

//任務(wù)提交階段
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //判斷當(dāng)前workers中的線程數(shù)量有沒有超過核心線程數(shù)
    if (workerCountOf(c) < corePoolSize) {
        //如果沒有則創(chuàng)建核心線程數(shù)(參數(shù)true指的就是核心線程)
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //如果超過核心線程數(shù)了 先校驗(yàn)線程池是否正常運(yùn)行后向阻塞隊(duì)列workQueue末尾添加任務(wù)
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //再次檢查線程池運(yùn)行狀態(tài),若不在運(yùn)行則移除該任務(wù)并且執(zhí)行拒絕策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //若果沒有線程在執(zhí)行
        else if (workerCountOf(recheck) == 0)
            //則創(chuàng)建一個(gè)空的worker 該worker從隊(duì)列中獲取任務(wù)執(zhí)行
            addWorker(null, false);
    }
    //否則直接添加非核心線程執(zhí)行任務(wù) 若非核心線程也添加失敗 則執(zhí)行拒絕策略
    else if (!addWorker(command, false))
        reject(command);
}

線程創(chuàng)建:addWorker()方法

//addWorker通過cas保證了并發(fā)安全性
private boolean addWorker(Runnable firstTask, boolean core) {
        //第一部分 計(jì)數(shù)判斷,不符合返回false
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //判斷線程數(shù),最大29位(CAPACITY=29位二進(jìn)制),所以設(shè)置線程池的線程數(shù)不是任意大的
                if (wc >= CAPACITY ||
                    //判斷工作中的核心線程是否大于設(shè)置的核心線程或者設(shè)置的最大線程數(shù)
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //通過cas新增 若添加失敗會(huì)一直重試 若成功則跳過結(jié)束retry
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                //再次判斷運(yùn)行狀態(tài) 若運(yùn)行狀態(tài)改變則繼續(xù)重試
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        //第二部分:創(chuàng)建新的work放入works(一個(gè)hashSet)
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //將task任務(wù)封裝在新建的work中
            w = new Worker(firstTask);
            //獲取正在執(zhí)行該任務(wù)的線程
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //將work加入到workers中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //上述work添加成功了,就開始執(zhí)行任務(wù)操作了
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            //如果上述添加任務(wù)失敗了,會(huì)執(zhí)行移除該任務(wù)操作
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

獲取任務(wù)getTask()方法

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            //這里判斷是否要做超時(shí)處理,這里決定了當(dāng)前線程是否要被釋放
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //檢查當(dāng)前worker中線程數(shù)量是否超過max 并且上次循環(huán)poll等待超時(shí)了,則將隊(duì)列數(shù)量進(jìn)行原子性減
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                //線程可以被釋放,那就是poll,釋放時(shí)間就是keepAliveTime
                //否則,線程不會(huì)被釋放,take一直阻塞在這里,直至新任務(wù)繼續(xù)工作
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                //到這里說明可被釋放的線程等待超時(shí)卧秘,已經(jīng)銷毀呢袱,設(shè)置該標(biāo)記,下次循環(huán)將線程數(shù)減少
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

(6)注意點(diǎn)

線程池是如何保證不被銷毀的

當(dāng)隊(duì)列中沒有任務(wù)時(shí),核心線程會(huì)一直阻塞獲取任務(wù)的方法,直至獲取到任務(wù)再次執(zhí)行

線程池中的線程會(huì)處于什么狀態(tài)

WAITING , TIMED_WAITING ,RUNNABLE

核心線程與非核心線程有本質(zhì)區(qū)別嗎翅敌?

答案:沒有羞福。被銷毀的線程和創(chuàng)建的先后無關(guān)。即便是第一個(gè)被創(chuàng)建的核心線程蚯涮,仍然有可能被銷毀
驗(yàn)證:看源碼治专,每個(gè)works在runWork的時(shí)候去getTask,在getTask內(nèi)部遭顶,并沒有針對(duì)性的區(qū)分當(dāng)前work是否是核心線程或者類似的標(biāo)記张峰。只要判斷works數(shù)量超出core,就會(huì)調(diào)用poll()棒旗,否則take()

2.2 鎖

(1)鎖的分類

1)樂觀鎖/悲觀鎖

樂觀鎖顧名思義喘批,很樂觀地認(rèn)為每次讀取數(shù)據(jù)的時(shí)候總是認(rèn)為沒人動(dòng)過,所以不去加鎖铣揉。但是在更新的時(shí)候回去對(duì)比一下原來的值饶深,看有沒有被別人更改過。適用于讀多寫少的場景逛拱。mysql中類比version號(hào)更新java中的atomic包屬于樂觀鎖實(shí)現(xiàn)敌厘,即CAS(下節(jié)會(huì)詳細(xì)介紹)

悲觀鎖在每次讀取數(shù)據(jù)的時(shí)候都認(rèn)為其他人會(huì)修改數(shù)據(jù),所以讀取數(shù)據(jù)的時(shí)候也加鎖朽合,這樣別人想拿的時(shí)候就會(huì)阻塞额湘,直到這個(gè)線程釋放鎖,這就影響了并發(fā)性能旁舰。適合寫操作比較多的場景锋华。mysql中類比for update。synchronized實(shí)現(xiàn)就是悲觀鎖(1.6之后優(yōu)化為鎖升級(jí)機(jī)制)箭窜,悲觀鎖書寫不當(dāng)很容易影響性能毯焕。

2)獨(dú)享鎖/共享鎖

很好理解,獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有磺樱,而共享鎖是指該鎖可被多個(gè)線程所持有纳猫。
案例一:ReentrantLock,獨(dú)享鎖

public class PrivateLock {
   Lock lock = new ReentrantLock();
   long start = System.currentTimeMillis();
   void read() {
       lock.lock();
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
       System.out.println("read time = "+(System.currentTimeMillis() - start));
   }
   public static void main(String[] args) {
       final PrivateLock lock = new PrivateLock();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   lock.read();
               }
           }).start();
       }
   }
}

結(jié)果分析:每個(gè)線程結(jié)束的時(shí)間點(diǎn)逐個(gè)上升竹捉,鎖被獨(dú)享芜辕,一個(gè)用完下一個(gè),依次獲取鎖

image.png

案例二:ReadWriteLock块差,read共享侵续,write獨(dú)享

public class SharedLock {
   ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
   Lock lock = readWriteLock.readLock();
   long start = System.currentTimeMillis();
   void read() {
       lock.lock();
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
       System.out.println("end time = "+(System.currentTimeMillis() - start));
   }
   public static void main(String[] args) {
       final SharedLock lock = new SharedLock();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   lock.read();
               }
           }).start();
       }
   }
}

結(jié)果分析:每個(gè)線程獨(dú)自跑倔丈,各在100ms左右,證明是共享的

image.png

案例三:同樣是上例状蜗,換成writeLock

Lock lock = readWriteLock.writeLock();

結(jié)果分析:恢復(fù)到了1s時(shí)長需五,變?yōu)楠?dú)享

image.png

小結(jié):

  • 讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫轧坎,寫讀 宏邮,寫寫的過程是互斥的。
  • 獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的蜜氨,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享捎泻。

3)分段鎖

從Map一家子說起....
HashMap是線程不安全的飒炎,在多線程環(huán)境下,使用HashMap進(jìn)行put操作時(shí)族扰,可能會(huì)引起死循環(huán)厌丑,導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap渔呵。

于是有了HashT able怒竿,HashT able是線程安全的。但是HashT able線程安全的策略實(shí)在不怎么高明扩氢,將get/put所有相關(guān)操作都整成了synchronized的耕驰。

那有沒有辦法做到線程安全,又不這么粗暴呢录豺?基于分段鎖的ConcurrentHashMap誕生...

ConcurrentHashMap使用Segment(分段鎖)技術(shù)朦肘,將數(shù)據(jù)分成一段一段的存儲(chǔ),Segment數(shù)組的意義就是將一個(gè)大的table分割成多個(gè)小的table來進(jìn)行加鎖双饥,Segment數(shù)組中每一個(gè)元素一把鎖媒抠,每一個(gè)Segment元素存儲(chǔ)的是HashEntry數(shù)組+鏈表,這個(gè)和HashMap的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)一樣咏花。當(dāng)訪問其中一個(gè)段數(shù)據(jù)被某個(gè)線程加鎖的時(shí)候趴生,其他段的數(shù)據(jù)也能被其他線程訪問,這就使得ConcurrentHashMap不僅保證了線程安全昏翰,而且提高了性能苍匆。

但是這也引來一個(gè)負(fù)面影響:ConcurrentHashMap 定位一個(gè)元素的過程需要進(jìn)行兩次Hash操作,第一次Hash 定位到Segment棚菊,第二次Hash 定位到元素所在的鏈表浸踩。所以Hash 的過程比普通的HashMap 要長。

image.png

備注:JDK1.8ConcurrentHashMap中拋棄了原有的Segment 分段鎖统求,而采用了 CAS + synchronized來保證并發(fā)安全性检碗。

4)可重入鎖

可重入鎖指地獲取到鎖后据块,如果同步塊內(nèi)需要再次獲取同一把鎖的時(shí)候,直接放行后裸,而不是等待瑰钮。其意義在于防止死鎖冒滩。前面使用的synchronized 和ReentrantLock 都是可重入鎖微驶。

實(shí)現(xiàn)原理實(shí)現(xiàn)是通過為每個(gè)鎖關(guān)聯(lián)一個(gè)請(qǐng)求計(jì)數(shù)器和一個(gè)占有它的線程。如果同一個(gè)線程再次請(qǐng)求這個(gè)鎖开睡,計(jì)數(shù)器將遞增因苹,線程退出同步塊,計(jì)數(shù)器值將遞減篇恒。直到計(jì)數(shù)器為0鎖被釋放扶檐。

場景見于父類和子類的鎖的重入(調(diào)super方法),以及多個(gè)加鎖方法的嵌套調(diào)用胁艰。

案例一:父子可重入

public class ParentLock {
   byte[] lock = new byte[0];
   public void f1(){
       synchronized (lock){
           System.out.println("f1 from parent");
       }
   }
}

public class SonLock extends ParentLock {
   public void f1() {
       synchronized (super.lock){
           super.f1();
           System.out.println("f1 from son");
       }
   }
   public static void main(String[] args) {
       SonLock lock = new SonLock();
       lock.f1();
   }
}

案例二:內(nèi)嵌方法可重入

public class NestedLock {
   public synchronized void f1(){
       System.out.println("f1");
   }
   public synchronized void f2(){
       f1();
       System.out.println("f2");
   }
   public static void main(String[] args) {
       NestedLock lock = new NestedLock();
       //可以正常打印 f1,f2
       lock.f2();
   }
}

5)公平鎖/非公平鎖

基本概念:
公平鎖就是在并發(fā)環(huán)境中款筑,每個(gè)線程在獲取鎖時(shí)會(huì)先查看此鎖維護(hù)的等待隊(duì)列,如果為空腾么,或者當(dāng)前線程是等待隊(duì)列的第一個(gè)奈梳,就占有鎖,否則就會(huì)加入到等待隊(duì)列中解虱,直到按照FIFO的規(guī)則從隊(duì)列中取到自己攘须。

非公平鎖與公平鎖基本類似,只是在放入隊(duì)列前先判斷當(dāng)前鎖是否被線程持有殴泰。如果鎖空閑于宙,那么他可以直接搶占,而不需要判斷當(dāng)前隊(duì)列中是否有等待線程悍汛。只有鎖被占用的話捞魁,才會(huì)進(jìn)入排隊(duì)。在現(xiàn)實(shí)中想象一下游樂場旋轉(zhuǎn)木馬插隊(duì)現(xiàn)象......

優(yōu)缺點(diǎn):
公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死离咐,進(jìn)入隊(duì)列規(guī)規(guī)矩矩的排隊(duì)谱俭,遲早會(huì)輪到。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低健霹,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞旺上,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖的性能要高于公平鎖糖埋,因?yàn)榫€程有幾率不阻塞直接獲得鎖宣吱。ReentrantLock默認(rèn)使用非公平鎖就是基于性能考量。但是非公平鎖的缺點(diǎn)是可能引發(fā)隊(duì)列中的線程始終拿不到鎖瞳别,一直排隊(duì)被餓死征候。

編碼方式:
很簡單杭攻,ReentrantLock支持創(chuàng)建公平鎖和非公平鎖(默認(rèn)),想要實(shí)現(xiàn)公平鎖疤坝,使用new ReentrantLock(true)兆解。

背后原理:
AQS,后面還會(huì)詳細(xì)講到跑揉。AQS中有一個(gè)state標(biāo)識(shí)鎖的占用情況锅睛,一個(gè)隊(duì)列存儲(chǔ)等待線程。
state=0表示鎖空閑历谍。如果是公平鎖现拒,那就看看隊(duì)列有沒有線程在等,有的話不參與競爭乖乖追加到尾部望侈。如果是非公平鎖印蔬,那就直接參與競爭,不管隊(duì)列有沒有等待者脱衙。
state>0表示有線程占著鎖侥猬,這時(shí)候無論公平與非公平,都直接去排隊(duì)(想搶也沒有)
備注:
因?yàn)镽eentrantLock是可重入鎖捐韩,數(shù)量表示重入的次數(shù)退唠。所以是>0而不是簡單的0和1而synchronized只能是非公平鎖

6)鎖升級(jí)

java中每個(gè)對(duì)象都可作為鎖,鎖有四種級(jí)別奥帘,按照量級(jí)從輕到重分為:無鎖铜邮、偏向鎖、輕量級(jí)鎖寨蹋、重量級(jí)鎖松蒜。

如何理解呢?A占了鎖已旧,B就要阻塞等秸苗。但是,在操作系統(tǒng)中运褪,阻塞就要存儲(chǔ)當(dāng)前線程狀態(tài)惊楼,喚醒就要再恢復(fù),這個(gè)過程是要消耗時(shí)間的...

如果A使用鎖的時(shí)間遠(yuǎn)遠(yuǎn)小于B被阻塞和掛起的執(zhí)行時(shí)間秸讹,那么我們將B掛起阻塞就相當(dāng)?shù)牟缓纤恪?/p>

于是出現(xiàn)自旋:自旋指的是鎖已經(jīng)被其他線程占用時(shí)檀咙,當(dāng)前線程不會(huì)被掛起,而是在不停的試圖獲取鎖(可以理解為不停的循環(huán))璃诀,每循環(huán)一次表示一次自旋過程弧可。顯然這種操作會(huì)消耗CPU時(shí)間,但是相比線程下文切換時(shí)間要少的時(shí)候劣欢,自旋劃算棕诵。

而偏向鎖裁良、輕量鎖、重量鎖就是圍繞如何使得cpu的占用更劃算而展開的校套。

舉個(gè)生活的例子价脾,假設(shè)公司只有一個(gè)會(huì)議室(共享資源)
偏向鎖:
前期公司只有1個(gè)團(tuán)隊(duì),那么什么時(shí)候開會(huì)都能滿足笛匙,就不需要詢問和查看會(huì)議室的占用情況侨把,直接進(jìn)入使用
狀態(tài)。會(huì)議室門口掛了個(gè)牌子寫著A使用膳算,A默認(rèn)不需要預(yù)約(ThreadID=A)
輕量級(jí)鎖:
隨著業(yè)務(wù)發(fā)展座硕,擴(kuò)充為2個(gè)團(tuán)隊(duì)弛作,B團(tuán)隊(duì)肯定不會(huì)同意A無法無天涕蜂,于是當(dāng)AB同時(shí)需要開會(huì)時(shí),兩者競爭映琳,誰搶
到誰算誰的机隙。偏向鎖升級(jí)為輕量級(jí)鎖,但是未搶到者在門口會(huì)不停敲門詢問(自旋萨西,循環(huán))有鹿,開完沒有?開完
沒有谎脯?
重量級(jí)鎖:
后來發(fā)現(xiàn)葱跋,這種不停敲門的方式很煩,A可能不理不睬源梭,但是B要不停的鬧騰娱俺。于是鎖再次升級(jí)。
如果會(huì)議室被A占用废麻,那么B團(tuán)隊(duì)直接閉嘴荠卷,在門口安靜的等待(wait進(jìn)入阻塞),直到A用完后會(huì)通知
B(notify)烛愧。

注意點(diǎn):

  • 上面幾種鎖都是JVM自己內(nèi)部實(shí)現(xiàn)油宜,我們不需要干預(yù),但是可以配置jvm參數(shù)開啟/關(guān)閉自旋鎖怜姿、偏
    向鎖慎冤。
  • 鎖可以升級(jí),但是不能反向降級(jí):偏向鎖→輕量級(jí)鎖→重量級(jí)鎖
  • 無鎖爭用的時(shí)候使用偏向鎖沧卢,第二個(gè)線程到了升級(jí)為輕量級(jí)鎖進(jìn)行競爭蚁堤,更多線程時(shí),進(jìn)入重量級(jí)鎖阻塞
image.png

7) 互斥鎖/讀寫鎖

典型的互斥鎖:synchronized搏恤,ReentrantLock违寿,讀寫鎖:ReadWriteLock 前面都用過了互斥鎖屬于獨(dú)享鎖湃交,讀寫鎖里的寫鎖屬于獨(dú)享鎖,而讀鎖屬于共享鎖

案例:互斥鎖用不好可能會(huì)失效藤巢,看一個(gè)典型的鎖不住現(xiàn)象搞莺!

public class ObjectLock {
   public static Integer i=0;
   public void inc(){
       synchronized (this){
           int j=i;
           try {
               Thread.sleep(100);
               j++;
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           i=j;
       }
   }
   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                 //重點(diǎn)!
                   new ObjectLock().inc();
               }
           }).start();
       }
       Thread.sleep(3000);
       //理論上10才對(duì)掂咒〔挪祝可是....
       System.out.println(ObjectLock.i);
   }
}

結(jié)果分析:每個(gè)線程內(nèi)都是new對(duì)象,所以this不是同一把鎖绍刮,結(jié)果鎖不住温圆,輸出1
1.this,換成static的i 變量試試孩革?
2.換成ObjectLock.class 試試岁歉?
3.換成String.class
4.去掉synchronized塊,外部方法上加static synchronized

2.3 原子操作(atomic)

(1)概念

原子(atom)本意是“不能被進(jìn)一步分割的最小粒子”膝蜈,而原子操作(atomic operation)意為"不可被中斷的一個(gè)或一系列操作" 锅移。類比于數(shù)據(jù)庫事務(wù),redis的multi饱搏。

(2)CAS

Compare And Set(或Compare And Swap)非剃,翻譯過來就是比較并替換,CAS操作包含三個(gè)操作數(shù)——內(nèi)存位置(V)推沸、預(yù)期原值(A)备绽、新值(B)。從第一視角來看鬓催,理解為:我認(rèn)為位置V 應(yīng)該是A肺素,如果是A,則將B 放到這個(gè)位置深浮;否則压怠,不要更改,只告訴我這個(gè)位置現(xiàn)在的值即可飞苇。

計(jì)數(shù)器問題發(fā)生歸根結(jié)底是取值和運(yùn)算后的賦值中間菌瘫,發(fā)生了插隊(duì)現(xiàn)象,他們不是原子的操作布卡。前面的計(jì)數(shù)器使用加鎖方式實(shí)現(xiàn)了正確計(jì)數(shù)雨让,下面,基于CAS的原子類上場....

public class AtomicCounter {
   private static AtomicInteger i = new AtomicInteger(0);
   public int get(){
       return i.get();
   }
   public void inc(){
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       i.incrementAndGet();
   }
   public static void main(String[] args) throws InterruptedException {
       final AtomicCounter counter = new AtomicCounter();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   counter.inc();
               }
           }).start();
       }
       Thread.sleep(3000);
       //同樣可以正確輸出10
       System.out.println(counter.i.get());
   }
}

(3)atomic

上面展示了AtomicInteger忿等,關(guān)于atomic包栖忠,還有很多其他類型:

(4)基本類型

AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

(5)引用類型

AtomicReference : 原子更新引用類型
AtomicReferenceFieldUpdater :原子更新引用類型的字段
AtomicMarkableReference : 原子更新帶有標(biāo)志位的引用類型

(6)數(shù)組

AtomicIntegerArray:原子更新整型數(shù)組里的元素庵寞。
AtomicLongArray:原子更新長整型數(shù)組里的元素狸相。
AtomicReferenceArray:原子更新引用類型數(shù)組里的元素。

(7)字段

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器捐川。
AtomicLongFieldUpdater:原子更新長整型字段的更新器脓鹃。
AtomicStampedReference:原子更新帶有版本號(hào)的引用類型。

(8)注意

使用atomic要注意原子性的邊界古沥,把握不好會(huì)起不到應(yīng)有的效果瘸右,原子性被破壞。

public class BadAtomic {
   AtomicInteger i = new AtomicInteger(0);
   static int j=0;
   public void badInc(){
       int k = i.incrementAndGet();
       try {
           Thread.currentThread().sleep(new Random().nextInt(100));
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       j=k;
   }
   public static void main(String[] args) throws InterruptedException {
       BadAtomic atomic = new BadAtomic();
       for (int i = 0; i < 10; i++) {
           new Thread(()->{
               atomic.badInc();
           }).start();
       }
       Thread.sleep(3000);
       System.out.println(atomic.j);
   }
}

結(jié)果分析:

每次都不一樣岩齿,總之不是10
在badInc上加synchronized太颤,問題解決

這章節(jié)目前就介紹這么多,后續(xù)將擴(kuò)展更多的多線程相關(guān)的類,以及從項(xiàng)目中解讀多線程的應(yīng)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盹沈,一起剝皮案震驚了整個(gè)濱河市龄章,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌襟诸,老刑警劉巖瓦堵,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異歌亲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)澜驮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門陷揪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人杂穷,你說我怎么就攤上這事悍缠。” “怎么了耐量?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵飞蚓,是天一觀的道長。 經(jīng)常有香客問我廊蜒,道長趴拧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任山叮,我火速辦了婚禮著榴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屁倔。我一直安慰自己脑又,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著问麸,像睡著了一般往衷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上严卖,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天炼绘,我揣著相機(jī)與錄音,去河邊找鬼妄田。 笑死俺亮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疟呐。 我是一名探鬼主播脚曾,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼启具!你這毒婦竟也來了本讥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鲁冯,失蹤者是張志新(化名)和其女友劉穎拷沸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體薯演,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撞芍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了跨扮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片序无。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖衡创,靈堂內(nèi)的尸體忽然破棺而出帝嗡,到底是詐尸還是另有隱情,我是刑警寧澤璃氢,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布哟玷,位于F島的核電站,受9級(jí)特大地震影響一也,放射性物質(zhì)發(fā)生泄漏巢寡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一塘秦、第九天 我趴在偏房一處隱蔽的房頂上張望讼渊。 院中可真熱鬧,春花似錦尊剔、人聲如沸爪幻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挨稿。三九已至仇轻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奶甘,已是汗流浹背篷店。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臭家,地道東北人疲陕。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像钉赁,于是被迫代替她去往敵國和親蹄殃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1你踩、線程的實(shí)現(xiàn) 繼承Thread類:在java.lang包中定義诅岩,繼承Thread類必須重寫run()方法,然后通...
    與搬磚有關(guān)的日子閱讀 880評(píng)論 0 4
  • 1. 線程和進(jìn)程的區(qū)別带膜? 它們是不同的操作系統(tǒng)資源管理方式吩谦。進(jìn)程有獨(dú)立的地址空間,一個(gè)進(jìn)程崩潰后膝藕,在保護(hù)模式下不會(huì)...
    Darkmoss閱讀 531評(píng)論 0 0
  • 1式廷、什么是線程? 1)線程是輕量級(jí)的進(jìn)程 2)線程沒有獨(dú)立的地址空間(內(nèi)存空間) 3)線程由進(jìn)程創(chuàng)建(寄生在進(jìn)程)...
    夏與清風(fēng)閱讀 3,162評(píng)論 0 1
  • Java 多線程主要是實(shí)現(xiàn)方式有: 1.Thread 子類繼承Thread束莫,成為Thread的子類懒棉,調(diào)用start...
    LT_9999閱讀 261評(píng)論 0 0
  • 目前,多線程編程可以說是在大部分平臺(tái)和應(yīng)用上都需要實(shí)現(xiàn)的一個(gè)基本需求览绿。本系列文章就來對(duì) Java 平臺(tái)下的多線程編...
    業(yè)志陳閱讀 729評(píng)論 0 4