java多線程操作

概述

在過去單CPU時代她倘,單任務在一個時間點只能執(zhí)行單一程序擂找。之后發(fā)展到多任務階段迄损,計算機能在同一時間點并行執(zhí)行多任務或多進程奈应。雖然并不是真正意義上的“同一時間點”澜掩,而是多個任務或進程共享一個CPU,并交由操作系統(tǒng)來完成多任務間對CPU的運行切換杖挣,以使得每個任務都有機會獲得一定的時間片運行肩榕。

Java是最先支持多線程的開發(fā)的語言之一,Java從一開始就支持了多線程能力程梦,因此Java開發(fā)者能常遇到上面描述的問題場景

一点把、相關概念

  • 程序與進程
    程序是一組有序指令的集合橘荠,是一種靜態(tài)的概念。進程是程序的一次執(zhí)行郎逃,屬于一種動態(tài)的概念哥童。在多道程序環(huán)境下,程序的執(zhí)行屬于并發(fā)執(zhí)行褒翰,此時它們將失去封閉性贮懈,并具有間斷性,運行結果也將不可再現优训,為了能使多個程序可以并發(fā)執(zhí)行朵你,提高資源利用率和系統(tǒng)吞吐量,并且可以對并發(fā)執(zhí)行的程序加以描述和控制揣非,引入進程的概念抡医。
  • 進程和線程
    線程的引入主要是為了減少程序在并發(fā)執(zhí)行時所付出的時空開銷。我們知道早敬,為了能使程序能夠并發(fā)執(zhí)行忌傻,系統(tǒng)必須進行創(chuàng)建進程、撤銷進程以及進程切換等操作搞监,而進程作為一個資源的擁有者水孩,在進行這些操作時必須為之付出較大的時空開銷。
    線程和進程的區(qū)別主要如下:(1) 進程是系統(tǒng)中擁有資源的一個基本單位琐驴,線程本身并不擁有系統(tǒng)資源俘种,同一進程內的線程共享進程擁有的資源。(2) 進程僅是資源分配的基本單位绝淡,線程是調度和分派的基本單位宙刘。(3) 進程之間相對比較獨立,彼此不會互相影響够委,而線程共享同一個進程下面的資源荐类,可以互相通信影響。(4) 線程的并發(fā)性更高茁帽,可以啟動多個線程執(zhí)行同程序的不同部分玉罐。
  • 并行和并發(fā)
    并行是指兩個或多個線程在同一時刻執(zhí)行,并發(fā)是指兩個或多個線程在 同一時間間隔 內發(fā)生潘拨。如果程序同時開啟的線程數小于CPU的核數吊输,那么不同進程的線程就可以分配給不同的CPU來運行,這就是并行铁追,如果線程數多于CPU的核數季蚂,那就需要并發(fā)技術。

二、Java多線程

Java虛擬機允許應用程序并發(fā)地運行多個執(zhí)行線程扭屁,常見的開啟新的線程的方法主要有4種算谈。

  • (常用)任務類實現Runnable接口,在方法Run()里定義任務料滥。
public class Main {

    public static void main(String[] args) {

       //將ThreadNew實例作為參數實例化Thread之后start啟動線程
      //Thread構造器接收Runnable接口實例
        new Thread(new ThreadNew()).start();

        System.out.println(" Thread Main ");
    }
}

// 實現Runnable接口并在方法run里定義任務
class ThreadNew implements Runnable {

    @Override
    public void run() {

        try { // 延時0.5秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(" Thread New ");
    }
}

  • 任務類集成Thread然眼,重寫run()方法
public class Main {

    public static void main(String[] args) {

        new ThreadNew2().start();

        System.out.println(" Thread Main ");
    }
}

// 繼承自類Thread并重寫run方法
class ThreadNew2 extends Thread {

    @Override
    public void run() {
        try { // 延時0.5秒
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" Thread New2 ");
    }
}
  • 實現接口Callable并在call()方法里得到線程執(zhí)行結果。
public class Main {

    public static void main(String[] args) {

        FutureTask<String> futureTask = new FutureTask<>(new ThreadNew3());

        new Thread(futureTask).start();

        System.out.println(" Thread Main ");

        try {
            System.out.println("執(zhí)行結果是 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

// 實現接口Callable并在call()方法里定義任務
class ThreadNew3 implements Callable<String> {

    @Override
    public String call() throws Exception {

        try { // 延時0.5秒
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" Thread New3 ");

        return "Thread New3 Result";
    }
}
  • 通過線程池創(chuàng)建線程
    上面4種就是Java中開啟新的線程的方式葵腹,其中第1種高每,實現Runnable接口最常用,也最靈活践宴,第2種鲸匿,因為任務類必須繼承自Thread,而Java中又僅支持單繼承阻肩,所以有時不太方便带欢,第3種方法主要是可以得到線程執(zhí)行的返回結果。

開啟的新線程都有一個線程優(yōu)先級烤惊,代表該線程的重要程度洪囤,可以通過Thread類的getPriority()和setPriority()來得到或者設置線程的優(yōu)先級。線程的優(yōu)先級范圍是1~10撕氧,默認情況下是5。

在線程創(chuàng)建完成還未啟動的時候喇完,我們可以通過方法setDaemon()來將線程設置為守護線程伦泥。守護線程,簡單理解為后臺運行線程锦溪,比如當程序運行時播放背景音樂。守護線程與普通線程在寫法上基本沒有區(qū)別,需要注意的是舵揭,當進程中所有非守護線程已經結束或者退出的時候如筛,即使還有守護線程在運行,進程仍然將結束则涯。

  • 終止線程
    Java沒有提供任何機制來安全地終止線程复局,那么怎么使線程停止或者中斷呢?
  1. 線程自己在run()方法執(zhí)行完后自動終止(安全的方式)
  2. 調用Thread.stop()方法強迫停止一個線程粟判,不過此方法是不安全的亿昏,已經不再建議使用。(不安全方式)
  3. 比較安全可靠的是利用Java的中斷機制档礁,使用方法Thread.interrupt()角钩。需要注意的是,通過中斷并不能直接終止另一個線程,需要被中斷的線程自己處理中斷递礼。被終止的線程一定要添加代碼對isInterrupted狀態(tài)進行處理惨险,否則即使代碼是死循環(huán)的情況下,線程也將永遠不會結束脊髓。(安全方式)

三辫愉、鎖機制

  • synchronized 同步鎖
    synchronized,是Java里面的一個關鍵詞供炼,當它用來修飾一個方法或者一個代碼塊的時候一屋,能夠保證在同一時刻最多只有一個線程執(zhí)行該段代碼。用法如下:
寫法一袋哼、修飾在方法上

    public synchronized void add1() {

    }

寫法二冀墨、修飾在代碼塊上

    public void add2() {
//這里的this指的是執(zhí)行這段代碼的對象
        synchronized (this) {

        }
    }

寫法三、指定一個小的對象值進行加鎖

    private byte[] lock = new byte[1];

    public void add3() {
        synchronized (lock) {

        }
    }

上面synchronized三種寫法中涛贯,最后一種性能和執(zhí)行效率最高诽嘉,synchronized修飾方法上的效率最低。原因主要是作用在方法體上的話弟翘,即使獲得了鎖那么進入方法體內分配資源還是需要一定時間的虫腋。前兩種鎖的對象都是對象本身,加鎖和釋放鎖都需要此對象的資源稀余,那么自己造一個byte對象悦冀,可以提升效率。
關于sychronized的詳細用法睛琳,可以查看這篇博文

  • ReentrantLock
    在介紹ReentrantLock之前盒蟆,我們先看一個接口Lock。

Lock提供比synchronized更豐富师骗,更靈活的鎖操作历等。Lock的實現類比synchronized更靈活,但是必須手動釋放和開啟鎖辟癌,適用于代碼塊鎖寒屯,synchronized對象之間是互斥關系。

ReentrantLock是接口Lock的一個具體實現類黍少。當許多線程視圖訪問ReentrantLock保護的共享資源時寡夹,JVM將花費較少的時間來調度線程,用更多的時間執(zhí)行線程仍侥。它的用法主要如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {

        lock.lock();  // 開啟鎖

        try {
            //方法體
        } finally {
            lock.unlock();//釋放鎖
        }
    }
}
  • volatile關鍵字
    一旦一個共享變量(類的成員變量要出、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
    1)保證了不同線程對這個變量進行操作時的可見性农渊,即一個線程修改了某個變量的值患蹂,這新值對其他線程來說是立即可見的或颊。
    2)禁止進行指令重排序。
    下面我們看一下這個例子:
public class Counter {
 
    public volatile static int count = 0;
 
    public static void inc() {
 
        //這里延遲1毫秒传于,使得結果明顯
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
    }
 
    public static void main(String[] args) {
 
        //同時啟動1000個線程囱挑,去進行i++計算,看看實際結果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //這里每次運行的值都有可能不同,可能為1000
        System.out.println("運行結果:Counter.count=" + Counter.count);
    }
}

許多人認為加入volatile關鍵字之后沼溜,我們得到的最終值會是1000平挑,但實際上為Counter.count=992。
volatile的應用場景 https://blog.csdn.net/vking_wang/article/details/9982709


為什么會出現這種情況呢系草?

我們知道通熄,在jvm中,每一個線程運行時都有一個線程棧找都,線程棧保存了線程運行時候變量值信息唇辨。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值能耻,然后把堆內存變量的具體值load到線程本地內存中赏枚,建立一個變量副本,之后線程就不再和對象在堆內存變量值有任何關系晓猛,而是直接修改副本變量的值饿幅,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量戒职。這樣在堆中的對象的值就產生變化了栗恩。

這里可以用AtomicInteger來聲明count,它通過CAS算法保證了線程的安全性

read and load 從主存復制變量到當前工作內存
use and assign 執(zhí)行代碼洪燥,改變共享變量值
store and write 用工作內存數據刷新主存相關內容
但是在read load之后摄凡,如果主內存count變量發(fā)生修改之后,線程工作內存中的值由于已經加載蚓曼,不會產生對應的變化,所以計算出來的結果會和預期不一樣

四钦扭、線程池

Java通過Excutor提供4種線程池纫版,分別為:

  • newCachedThreadPool創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要客情,可靈活回收空閑線程其弊,若無可回收,則新建線程膀斋。
  • newFixedThreadPool 創(chuàng)建一個定長線程池梭伐,可控制線程最大并發(fā)數,超出的線程會在隊列中等待仰担。
  • newScheduledThreadPool 創(chuàng)建一個定長線程池糊识,支持定時及周期性任務執(zhí)行。
  • newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務赂苗,保證所有任務按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行愉耙。

1. newCachedThreadPool

創(chuàng)建一個可緩存(可擴展)線程池,如果線程長度超過處理需求拌滋,可靈活回收空閑線程朴沿,若無可回收的,則新建線程

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
    try {
        Thread.sleep(index * 1000);
    } 
        catch (InterruptedException e) {
            e.printStackTrace();
    }

cachedThreadPool.execute(new Runnable() {

@Override
public void run() {
    System.out.println(index);
}
});
}

線程池為無限大败砂,當執(zhí)行第二個任務時第一個任務已經完成赌渣,會復用執(zhí)行第一個任務的線程,而不用每次新建線程昌犹。
從jconsole中坚芜,我們可以看到線程數后來在程序運行中維持不變


活動線程數.JPG

2. newFixedThreadPool

創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數祭隔,超出的線程會在隊列中等待

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 10; i++) {
    final int index = i;

    fixedThreadPool.execute(new Runnable() {

@Override
public void run() {
try {
    System.out.println(index);
    Thread.sleep(2000);
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
}
});
}

因為線程池大小為3货岭,每個任務輸出index后sleep 2秒,所以每兩秒打印3個數字疾渴。定長線程池的大小最好根據系統(tǒng)資源進行設置千贯。

3. newScheduledThreadPool

創(chuàng)建一個定長線程池,支持定時及周期性任務執(zhí)行搞坝。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {
  @Override
  public void run() {
      System.out.println("delay 3 seconds");
    }
  }, 3, TimeUnit.SECONDS);

表示延遲1秒后每3秒執(zhí)行一次搔谴。
ScheduledExecutorService比Timer更安全,功能更強大

4. newSingleThreadExecutor

創(chuàng)建一個單線程化的線程池桩撮,它只會用唯一的工作線程來執(zhí)行任務敦第,保證所有任務按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。示例代碼如下

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {

@Override
public void run() {
    try {
        System.out.println(index);
    Thread.sleep(2000);
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
        }
}
    });
}

結果依次輸出店量,相當于順序執(zhí)行各個任務芜果。

現行大多數GUI程序都是單線程的。Android中單線程可用于數據庫操作融师,文件操作右钾,應用批量安裝,應用批量刪除等不適合并發(fā)但可能IO阻塞性及影響UI線程響應的操作旱爆。


為什么要使用線程池:

  1. 減少了創(chuàng)建和銷毀線程的次數舀射,每個工作線程都可以被重復利用,可執(zhí)行多個任務怀伦。
  2. 可以根據系統(tǒng)的承受能力脆烟,調整線程池中工作線線程的數目,防止因為消耗過多的內存房待,而把服務器累趴下(每個線程需要大約1MB內存邢羔,線程開的越多驼抹,消耗的內存也就越大,最后死機)张抄。

Java里面線程池的頂級接口是Executor砂蔽,但是嚴格意義上講Executor并不是一個線程池,而只是一個執(zhí)行線程的工具署惯。真正的線程池接口是ExecutorService左驾。
比較重要的幾個類
ExecutorService: 真正的線程池接口。
ScheduledExecutorService: 能和Timer/TimerTask類似极谊,解決那些需要任務重復執(zhí)行的問題诡右。
傳統(tǒng)的timer的缺點:Timer對任務的調度是基于絕對時間的;所有的TimerTask只有一個線程TimerThread來執(zhí)行轻猖,因此同一時刻只有一個TimerTask在執(zhí)行帆吻;任何一個TimerTask的執(zhí)行異常都會導致Timer終止所有任務;由于基于絕對時間并且是單線程執(zhí)行咙边,因此在多個任務調度時猜煮,長時間執(zhí)行的任務被執(zhí)行后有可能導致短時間任務快速在短時間內被執(zhí)行多次或者干脆丟棄多個任務。
ThreadPoolExecutor: ExecutorService的默認實現败许。
ScheduledThreadPoolExecutor: 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現王带,周期性任務調度的類實現。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末市殷,一起剝皮案震驚了整個濱河市愕撰,隨后出現的幾起案子,更是在濱河造成了極大的恐慌醋寝,老刑警劉巖搞挣,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異音羞,居然都是意外死亡囱桨,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門嗅绰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝇摸,“玉大人,你說我怎么就攤上這事办陷。” “怎么了律歼?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵民镜,是天一觀的道長。 經常有香客問我险毁,道長制圈,這世上最難降的妖魔是什么们童? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鲸鹦,結果婚禮上慧库,老公的妹妹穿的比我還像新娘。我一直安慰自己馋嗜,他們只是感情好齐板,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葛菇,像睡著了一般甘磨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上眯停,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天济舆,我揣著相機與錄音,去河邊找鬼莺债。 笑死滋觉,一個胖子當著我的面吹牛,可吹牛的內容都是我干的齐邦。 我是一名探鬼主播椎侠,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侄旬!你這毒婦竟也來了肺蔚?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤儡羔,失蹤者是張志新(化名)和其女友劉穎宣羊,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體汰蜘,經...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡仇冯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了族操。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苛坚。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖色难,靈堂內的尸體忽然破棺而出泼舱,到底是詐尸還是另有隱情,我是刑警寧澤枷莉,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布娇昙,位于F島的核電站,受9級特大地震影響笤妙,放射性物質發(fā)生泄漏冒掌。R本人自食惡果不足惜噪裕,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望股毫。 院中可真熱鬧膳音,春花似錦、人聲如沸铃诬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氧急。三九已至颗胡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吩坝,已是汗流浹背毒姨。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钉寝,地道東北人弧呐。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像嵌纲,于是被迫代替她去往敵國和親俘枫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355