Java多線程編程核心技術讀書筆記

第一章 JAVA多線程技能

實現(xiàn)多線程編程的方式主要有兩種。

  • 繼承Thread類

  • 實現(xiàn)Runable接口

    工作時的性質(zhì)相同,主要是Java不能支持多繼承。

    繼承Thread類后,執(zhí)行start()方法的順序不代表線程啟動的順序。

如何使用實現(xiàn)了MyRunable的類呢?可以看一下Thread.java的構造函數(shù)

以下是一個使用實例:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("運行中!");
    }
}
public class Run {
    public static void main(String[] args){
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("運行結束");
    }
}

運行結果:

image-20190108102448065

主要是通過創(chuàng)建Thread對象,將實現(xiàn)了run()方法的對象傳入Thread。

線程安全

非線程安全:主要是指多個線程對同一個對象中的同一個實例變量進行操作時,出現(xiàn)值被更改、值不同步

可通過synchronized關鍵字給任意對象或者方法加鎖以達到線程安全的目的。

Thread.currentThread()和this區(qū)別

Thread.currentThread()指的是正在執(zhí)行操作的線程,this則是指向的線程對象的線程。

run()start()區(qū)別

? run()是將run()方法交給當前線程執(zhí)行,與主線程是同步執(zhí)行。

? start()則是另啟線程執(zhí)行方法,與主線程是異步執(zhí)行。

停止線程

  1. 使用退出標志使線程正常退出,也就是當run()方法完成后線程終止。
  2. 使用stop()方法強行終止線程,但是不推薦使用,是過期作廢的方法,且會終止正在運行中的線程。
  3. 使用interrupt()中斷線程。

interrupt()其實是標志了一個中斷狀態(tài),通過判斷這個狀態(tài)終止線程;

這是三個使用例子:

if(this.interrupted()){
    break;
}
if(this.interrupted()){
    return;
}
if(this.interrupted()){
    throw new InterruptedException();
}

interrupted()方法具有檢驗中斷狀態(tài)并清除中斷標志的功能。

isInterrupted()不是Static,且該方法僅檢測中斷狀態(tài)不清除中斷標志。

sleep()方法后,也就是沉睡中被interrupt()會拋出異常且清除中斷標志,與之相反的操作也是一樣的結果。

stop()已經(jīng)被作廢,因為如果強制讓線程停止可能使清理性工作不能完成,且會對象進行解鎖導致數(shù)據(jù)不一致。

暫停線程

通過suspend()暫停線程,resume()方法恢復線程的執(zhí)行。

缺點一是獨占。如果使用不當,將造成公共的同步對象的獨占,使得其他線程無法訪問公 共同步對象。當線程獲取到鎖時,執(zhí)行了suspend()就將會造成獨占,鎖將無法被釋放。

有一個特別的坑,printf()方法內(nèi)部存在同步鎖,這點需要注意。

缺點二是不同步,容易出現(xiàn)因為線程的暫停而導致數(shù)據(jù)不同步的情況。

yield方法

yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去占用CPU執(zhí)行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。

線程的優(yōu)先級

CPU優(yōu)先執(zhí)行優(yōu)先級較高的線程對象中的任務。

設置優(yōu)先級可使用setPriorith(),JDK源碼如下:

image-20190108152443448

JAVA中線程優(yōu)先級分為1~10這10個等級,JDK中使用了3個常量來預置定義優(yōu)先級的值,代碼如下:

image-20190108152628336
線程優(yōu)先級的繼承特性

JAVA中線程的優(yōu)先級具有繼承性,比如A線程啟動B線程,則B線程的優(yōu)先級與A是一樣的。

優(yōu)先級具有規(guī)則性

高優(yōu)先級的線程總是大部分先執(zhí)行完,但不代表高優(yōu)先級的全部先執(zhí)行完。當線程優(yōu)先級差距很大時,誰先執(zhí)行完和代碼的調(diào)用順序無關。

優(yōu)先級具有隨機性

優(yōu)先級較高的線程不一定每一次都先執(zhí)行完。

守護線程

JAVA中存在兩種線程,一種是用戶線程,另一種是守護線程。

守護線程是一種特殊的線程,它的特性有“陪伴”的含義,當進程中不存在非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程。

第二章 對象及變量的并發(fā)訪問

synchronized同步方法

方法內(nèi)的變量為線程安全

方法中的變量不存在非線程安全問題,永遠都是線程安全的。這是方法內(nèi)部的變量是私的特性造成的。私有變量非共享,不被多線程修改,也就不存在線程安全問題。

實例變量非線程安全

這時候需要添加synchronized關鍵字。

多個對象多個鎖

synchronized鎖的是對象的代碼和方法,而不是一段代碼或者方法。

synchronized方法與鎖對象

當兩個線程訪問同一個對象的兩個方法時:

1. A線程先持有Object對象的Lock鎖,B線程可以以異步的方式調(diào)用Object 對象中的非synchronized類型的方法。
2. A線程先持有Object對象的Lock鎖,B線程如果在這是調(diào)用Object對象中的synchronized類型的方法則需等待,也就是同步。
臟讀

解決同一個對象的臟讀問題可在對象的get()set()都加上synchronized關鍵字。

synchronized鎖重入

關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程得到一個對象鎖后,再次請求此對象鎖時時可以再次得到該對象的鎖的。這也證明在一個synchronized方法/塊的內(nèi)部調(diào)用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。

個人理解就是得到鎖的線程最優(yōu)先處理,直到完成該線程的任務。

出現(xiàn)異常,鎖自動被釋放

這也是為了防止死鎖的發(fā)生。

synchronized不具有繼承性

比如子類調(diào)用父類方法,父類方法中的synchronized將會失效。

synchronized同步語句塊

? 顧名思義,可以鎖住代碼塊,使用例子如下:

synchronized(this){
    需要鎖住的代碼
}
synchronized(this)也是鎖定當前對象的

this是用來指向?qū)ο蟊O(jiān)視器的。

如果鎖定代碼塊時,對象監(jiān)視器非同一個對象,如synchronized(方法內(nèi)的私有對象)則相當于不是同一個鎖,程序?qū)惒綀?zhí)行。以下是一個例子:

public class Service {
    private String usernameParam;
    private String passwordParam;
//    private String anyString = new String(); //如果是對象監(jiān)視器是這個對象則同步

    public void setUsernamePassword(String username,String password){
        try {
            String anyString = new String(); //方法內(nèi)的私有對象作為對象監(jiān)視器,程序?qū)惒秸{(diào)用
            synchronized (anyString){
                System.out.println("線程名稱為: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 進入同步塊 ");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("線程名稱為: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 離開同步塊 ");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
對象監(jiān)視器
對象監(jiān)視器:在Java中,每個對象和Class內(nèi)部都有一個鎖,Class廣義上也是一個單例對象,每個對象和Class會和一個監(jiān)視器關聯(lián),注意措辭,鎖是存在于對象內(nèi)部的數(shù)據(jù)結構,監(jiān)視器是一個獨立的結構但是和對象關聯(lián),相同點是對象一定有一個鎖也一定關聯(lián)一個監(jiān)視器。另外,監(jiān)視器是操控線程的,他會維持一個代碼數(shù)據(jù)區(qū)和線程隊列等,保證同一時刻只有一個線程訪問代碼數(shù)據(jù)區(qū),監(jiān)視器就是通過判斷對象里鎖來完成這個安全訪問的功能的。監(jiān)視器是比鎖更高層次的抽象。具體的操作流程是:當代碼進入同步區(qū)域時,找到對象關聯(lián)的監(jiān)視器,然后調(diào)用監(jiān)視器獲取鎖的方法,監(jiān)視器會讀取對象頭里面有關鎖的信息作為參數(shù),然后進行獲取鎖的操作,或是讓當前線程得到鎖,或是讓當前線程等待,當代碼退出同步區(qū)域時,找到對象關聯(lián)的監(jiān)視器,然后調(diào)用監(jiān)視器釋放鎖的操作,整個流程大致是這個樣子。另外,需要明白的是,所有代碼都隸屬于某個對象,非靜態(tài)方法好說,靜態(tài)方法是和Class對象關聯(lián)的,廣義上也是隸屬于某個對象的。這樣就能理解為什么多線程為什么能夠?qū)崿F(xiàn)同步了,因為多個線程執(zhí)行同一個監(jiān)視器管理的一份臨界資源,自然就能處理同步的細節(jié)了。

出處:https://blog.csdn.net/tales522/article/details/80853489

個人理解:將對象監(jiān)視器視為分配鎖的地方,一次只有一個線程可以進入。進入則獲取鎖,出門則釋放鎖。

線程調(diào)用同步方法的順序是隨機的

由于線程調(diào)用同步方法的順序是隨機的,將可能造成臟讀現(xiàn)象。比如一個List,A和B線程同時操作List Service類對其進行add()如果List為空,添加數(shù)據(jù)。在synchronized add()沒有設置對象監(jiān)視器的情況下,將有可能發(fā)生臟讀。

為了解決這種原因造成的臟讀,可以將對象監(jiān)視器設為實例變量。

比如在上個例子中將synchronized add()改為

public class ListService{
    public add(List list,String data){
        try{
            synchronized(list){
                list.add()
            }
        }
    }
}

不再同步方法而是改為同步代碼塊且將對象監(jiān)視器該為list,就可以解決這個臟讀問題。

對象監(jiān)視器的三個結論

x為非this對象。

1. 當多個線程同時執(zhí)行`synchronized(x)`同步代碼塊時呈同步效果。
2. 當其他線程執(zhí)行x對象中的synchronized同步方法時呈同步效果。
3. 當其他線程執(zhí)行x對象方法里的`synchronized(this)`代碼塊時也呈現(xiàn)同步效果。
靜態(tài)同步synchronized方法與synchronized(class)代碼塊

關鍵字synchronized還可以應用在static靜態(tài)方法上,是對當前的*.JAVA文件對應的Class類進行持鎖。synchronized關鍵字加到非static方法上時給對象上鎖。

synchronized(class)的作用與synchronized static一樣都是鎖住class類

數(shù)據(jù)類型String的常量池特性

常量池特性:

String a = "a";
String b = "a";
System.out.println(a == b);

輸出結果:
    true

當new String對象時,當后面的對象值與前面對象相同時,后面的對象將視為前面的對象,二者都是同一個對象。因此當synchronized(String對象)時,可能會發(fā)生例外,使用了同一個對象監(jiān)視器。所以在大多數(shù)的情況下,同步synchronized代碼塊都不使用String作為鎖對象,而改用其他,比如將synchronized(String對象)改為synchronized(Object對象)

多線程的死鎖

死鎖:不同的線程在等待根本不可能被釋放的鎖,從而導致所有的任務都無法繼續(xù)完成。

可以使用JDK自帶JCONSOLE工具來檢測是否有死鎖的現(xiàn)象。

鎖對象的改變

鎖對象的屬性即使改變,以同一個對象為鎖的運行結果還是同步的。(String對象比較特別,需要注意)

volatile關鍵字

voliatile的主要作用是使變量在多個線程間可見

作用是強制從公共堆棧中取得變量的值,而不是從線程私有數(shù)據(jù)棧中取得變量的值

解決異步死循環(huán)

先來看一個例子:

public class RunThread extends Thread {
    private boolean isRunning = true;
    public boolean isRunning(){
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        System.out.println("run");
        while (isRunning == true){
        }
        System.out.println("線程被停止了");
    }
}
public class Run {
    public static void main(String[] args){
        RunThread thread = new RunThread();
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.setRunning(false);
        System.out.println("already been set false");
    }
}

運行結果:

run
already been set false

這是IDEA運行后的結果。但是如果使用同樣的代碼運行在JVM設置為Server服務器的環(huán)境中,運行打印輸出相同,但是將會進入死循環(huán)。這是因為變量isRunning == true存在于公共堆棧及線程的私有堆棧中。在JVM設置為-SERVER模式時為了線程運行的效率,線程一直在私有堆棧中取得isRunning的值是true。而代碼thread.setRunning(false);雖然被執(zhí)行,更新的卻是公共堆棧中的isRunning變量值為false,所以就一直是死循環(huán)的狀態(tài)。

解決這樣的問題就要使用volatile關鍵字了,強制線程訪問isRunning這個變量時,從公共堆棧中取值。

修改RunThread代碼如下:

public class RunThread extends Thread {
    volatile private boolean isRunning = true;
    public boolean isRunning(){
        return isRunning;
    }
    public void setRunning(boolean running) {
        isRunning = running;
    }
    @Override
    public void run() {
        System.out.println("run");
        while (isRunning == true){
        }
        System.out.println("線程被停止了");
    }
}

問題就解決了。

兩張圖幫助理解:

程序的私有堆棧:

image-20190114155332470

讀取公共內(nèi)存:

image-20190114160322601

? volatile的缺點時不支持原子性(整個程序中的所有操作,要么全部完成,要么全部不完成,不可能停滯在中間某個環(huán)節(jié))。個人理解:volatile相當于給變量增加了synchronized。

對比volatile和synchronized
  1. volatile性能比synchronized好,volatile只能修飾于變量,而synchronized可以修飾方法及代碼塊。
  2. 多線程訪問volatile不會發(fā)生阻塞,而synchronized會出現(xiàn)阻塞。
  3. volatile能保證數(shù)據(jù)的可見性,但不能保證原子性;而synchronized可以保證原子性也可以間接保證可見效,因為它會將私有內(nèi)存和公有內(nèi)存中的數(shù)據(jù)做同步。
  4. volatile解決的事變量在多個線程之間的可見性;而synchronized解決的事多個線程之間訪問資源的同步性。

synchronized包含兩個特征:互斥性和可見性

線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。

volatile非原子的特性

例子:

public class Mythread extends Thread {

    volatile public static int count;
    private static void addConut(){
        for (int i = 0; i < 1000; i++) {
            count++;
        }
        System.out.println("count= " + count);
    }

    @Override
    public void run() {
        addConut();
    }
}
public class Run {
    public static void main(String[] args){
        Mythread[] mythreads = new Mythread[1000];
        for (int i = 0; i < 1000; i++) {
            mythreads[i] = new Mythread();
        }
        for (int i = 0; i < 1000; i++) {
            mythreads[i].start();
        }
    }
}

運行結果:

count= 986804
count= 987804
count= 988804
count= 989804
count= 990804
count= 991804
count= 992804
count= 993804
count= 994804
count= 995804
count= 996804
count= 997804
count= 998804

最終結果不是1000000。

更改Mythread類,使用synchronized代替volatile

public class Mythread extends Thread {
    public static int count;
    synchronized private static void addConut(){
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println("count= " + count);
    }

    @Override
    public void run() {
        addConut();
    }
}

運行結果:

count= 989000
count= 990000
count= 991000
count= 992000
count= 993000
count= 994000
count= 995000
count= 996000
count= 997000
count= 998000
count= 999000
count= 1000000

結果正確。

關鍵字volatile提示線程每次從共享內(nèi)存中讀取變量,而不是私有內(nèi)存。但如果修改實例變量中的數(shù)據(jù),如i++,這樣的操作其實并不是一個原子操作,也就是非線程安全的,容易出現(xiàn)臟數(shù)據(jù)。解決的辦法就是使用synchronized關鍵字。

變量在內(nèi)存中的工作過程:

變量在內(nèi)存中的工作過程
  1. read和load階段:從主工作內(nèi)存復制變量到當前線程工作內(nèi)存
  2. use和assign階段:執(zhí)行代碼,改變共享變量值
  3. store和write階段:用工作內(nèi)存數(shù)據(jù)刷新主內(nèi)存對應變量的值。

volatile只能保證1階段是實時的不出問題。2、3階段不能保證同步,這也是容易造成臟數(shù)據(jù)的原因。

使用原子類進行i++操作

除了在i++操作時進行synchronized關鍵字實現(xiàn)同步外,還可以使用AtomicInteger原子類進行實現(xiàn)。

需要注意的是原子類addAndGet方法是原子的,但方法和方法之間的調(diào)用卻不是原子的。解決這樣的問題必須要用同步。

第三章 線程間通信

wait()作用

wait()作用是使當前執(zhí)行代碼的線程進行等待,將當前線程置入“預執(zhí)行隊列中”,并且在 wait()所在的代碼行處停止執(zhí)行,直到接到通知或中斷為止。在調(diào)用wait()之前,線程必須獲得該對象的對象級別鎖。如果在調(diào)用wait()時線程沒有持有適當?shù)逆i,將拋出IllegalMonitorStateException異常,它是RuntimeException的一個子類,因此不需要TRY-CATCH進行捕捉。

notify()作用

notify()也要在同步方法或同步塊中調(diào)用,調(diào)用前線程也必須獲得該對象的對象級別鎖。如果在調(diào)用notify()時線程沒有持有適當?shù)逆i,將拋出IllegalMonitorStateException異常。該方法用來通知那些可能等待該對象的對象鎖的其他線程,如果有多個線程等待,則由線程規(guī)劃器隨機挑選出其中一個呈wait狀態(tài)的線程,對其發(fā)出notify,并使它獲取該對象的對象鎖。注意:執(zhí)行notify()后,當前線程不會馬上釋放該對象鎖,要等到執(zhí)行notify()方法的線程將程序執(zhí)行完。當?shù)谝粋€獲得了該對象鎖的wait線程運行完畢也后它會釋放掉該對象鎖,此時如果該對象沒有再次使用notify語句,則即便該對象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于沒有收到該對象通知,還會繼續(xù)阻塞在wait狀態(tài),直到該對象發(fā)出notify或notifyAll。

個人理解:notify使其他線程重新競爭鎖,而不是直接獲取鎖。

notifyAll()作用

notifyAll()notify()相同,區(qū)別是notify()只喚醒一個線程,notifyAll()喚醒等待該對象鎖的全部線程。

wait()遇到interrupt()

當線程呈wait()狀態(tài)時,調(diào)用interrupt()會出現(xiàn)InterruptedException異常。

生產(chǎn)者/消費者模式

等待/通知模式最經(jīng)典的案例就是“生產(chǎn)者/消費者”模式,遠離都是基于wait/notify。需要注意的是:wait條件的判斷最好使用while而不是if,否則在執(zhí)行POP時容易拋出異常。喚醒最好使用notifyAll()而不是notify()否則在連續(xù)喚醒同類線程的情況下將會出現(xiàn)“假死情況”。

通過管道進行線程間通信

可以通過管道流(pipeStream)用于在不同線程間直接傳送數(shù)據(jù),而無需借助類似臨時文件之類的東西。

Java的JDK中提供了4個類:

  1. PipedInputStream和PipedOutputStream
  2. PipedReader和PipedWriter

1.用來傳遞字節(jié)流,2.用來傳遞字符流。

方法join的使用

join方法的作用是使所屬的線程對象X正常執(zhí)行run()方法中的任務,而使當前線程z進行無限期的阻塞,等待線程X銷毀后再繼續(xù)執(zhí)行線程z后面的代碼,換種說法就是等待線程對象銷毀,常用于主線程等待子線程。

join(long)可以設置等待時間。

join和synchronized的區(qū)別是:join在內(nèi)部使用wait()方法進行等待,而synchronized關鍵字使用的是“對象監(jiān)視器”原理作為同步。

join(long)sleep(long)的區(qū)別

方法join(long)的功能在內(nèi)部是使用wait(long)來實現(xiàn)的,所以join(long)具有釋放鎖的特點。sleep(long)不具備釋放鎖的特點。

join與異常

在join過程中,如果當前線程對象被中斷,則當前線程出現(xiàn)異常。

join后面的代碼提前運行

類ThreadLocal的使用

主要解決的是每個線程綁定自己的值,可以將ThreadLocal比喻成全局存放數(shù)據(jù)的盒子,盒子中可以儲存每個線程的私有數(shù)據(jù)。

可以通過繼承ThreadLocal類,復寫initialValue()方法為類設置初始值。初始值也可以具有線程變量的隔離性。

類InheritableThreadLocal的使用

使用類InheritableThreadLocal可以在子線程中取得父線程繼承下來的值。

通過復寫childValue()可以繼承值并對值進行修改。

需要注意的一點是:如果子線程在取得值的同時,主線程將InheritableThreadLocal中的值進行更改,那么子線程取到的值還是舊值。

第四章 Lock的使用

ReentrantLock類

使用方法
lock();
doSomething(); //需要同步的代碼
unlock();

使用Condition實現(xiàn)等待/通知

Object類中的notify()方法相當于Condition類中的signal()方法。

Object類中的notifyAll()方法相當于Condition類中的signalAll()方法。

公平鎖和非公平鎖

公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶占機制,是隨機獲得鎖的。

默認情況下,ReentrantLock類使用的是非公平鎖。

使用方法:

Lock lock = new ReentrantLock(isFair) //isFair為true則為公平鎖

一些Lock類的常用方法

getHoldCount()、getQueueLength()、getWaitQueueLength()的功能

int getHoldCount():查詢當前線程保持此鎖定的個數(shù),也就是調(diào)用lock()方法的次數(shù)。

int getQueueLength():返回正等待獲取此鎖定的線程數(shù)。

int getWaitQueueLength():返回執(zhí)行了同一個condition.await()的線程數(shù)。

hasQueuedThread()、hasQueuedThreads()、hasWaiters()的功能

boolean hasQueuedThread(Thread thread):查詢指定線程是否在等待獲取此鎖定

boolean hasQueuedThreads():查詢是否有線程在等待獲取此鎖定

boolean hasWaiters(Condition condition):是否有線程正在等待與此鎖定有關的condition條件。

isFair()、isHeldByCurrentThread()、isLocked()的功能

boolean isFair():判斷是不是公平鎖

isHeldByCurrentThread():當前線程是否保持此鎖定

isLocked():此鎖定是否被線程保持

lockInterruptibly()tryLock()、tryLock(long timeout,TimeUnit unit)

lockInterruptibly():如果當前線程未被中斷,則獲取鎖定,如果已經(jīng)被中斷則出現(xiàn)異常

tryLock():僅在調(diào)用時鎖定未被另一個線程保持的情況下,才獲取該鎖定

tryLock(long timeout,TimeUnit unit):如果鎖定在給定等待時間內(nèi)沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖定。

awaitUninterruptibly()的使用

condition.awaitUninterruptibly()作用使該線程不可被中斷

awaitUnitl()的使用

condition.awaitUntil(Time time)相當于wait(Time time),可以被提前喚醒。

使用Condition實現(xiàn)順序執(zhí)行

使用Condition對象可以對線程執(zhí)行的業(yè)務進行排序規(guī)劃。

使用ReentrantReadWriteLock類

類ReentrantLock具有完全互斥排他的效果,即同一時間只有一個線程在執(zhí)行ReentrantLock.lock()方法后面的任務。這樣雖然保證了實例變量的線程安全性,但效率低下。所以JDK提供了一種讀寫鎖ReentrantReadWriteLock類,使用它可以加快運行效率。

讀寫鎖表示有兩個鎖,一個是讀操作相關的鎖,也稱為共享鎖;另一個是寫操作相關的鎖,也叫排他鎖。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。

“讀寫”、“寫讀”、“寫寫”都是互斥的;而“讀讀”是異步的,非互斥的。

簡單記憶:寫操作與任何操作互斥。

第五章 定時器

書上定時器這章介紹的是Timer類的使用,但Timer類存在許多問題,如果使用JDK的工具類來實現(xiàn)定時任務,阿里巴巴推薦使用ScheduledExecutorService類。

定時器類Timer的使用

JDK中Timer類主要負責計劃任務的功能。

Timer類的主要作用是設置計劃任務,但封裝任務的類是TimerTask類。

執(zhí)行計劃任務的代碼要放入TimerTask的子類中,因為TimerTask是一個抽象類。

方法schedule(TimerTask task,Date time)的使用

schedule()方法,都是按順序執(zhí)行。Task隊列中同一個Task只能存在一個,否則將會拋出異常!

該方法的作用是在指定的日期執(zhí)行一次某一任務。

這是一個使用例子:

public class RunSchedule {
    private static Timer timer = new Timer();
    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("運行時間為:" + new Date().toLocaleString());
        }
    }

    public static void main(String[] args){
        try {
            MyTask task = new MyTask();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = "2019-01-21 10:14:10";
            System.out.println("字符串時間為:" + dateString +  "當前時間為: " + new Date().toLocaleString());
            Date dateRef = sdf.parse(dateString);
            timer.schedule(task,dateRef);
        }catch (ParseException e){
            e.printStackTrace();
        }
    }
}

運行結果:

image-20190121101654425

任務雖然執(zhí)行完,但進程還未銷毀。這是因為創(chuàng)建一個Timer就是啟動一個新的線程,這個線程并不是守護線程,它一直在運行。

通過在

Timer timer = new Timer(True) //設置程序運行后迅速結束當前的進程。

方法schedule(TimerTask task,Date FirstTime,long period)的使用

該方法的作用是在指定的日期之后,按指定的間隔周期性地無限循環(huán)地執(zhí)行某一任務。

period:填的是間隔時間,以毫秒為單位。

兩種情況

計劃時間早于當前時間

? 如果執(zhí)行任務的時間早于當前時間,則立即執(zhí)行Task任務。

多個TimerTask任務及延時

? TimerTask是以隊列的方式一個一個被順序執(zhí)行,所以執(zhí)行的時間有可能和預期的時間不一致,因為前面的任務可能消耗的時間較長,則后面的任務運行的時間也會被延遲。

TimerTask類的cancel()方法

作用是將自身從任務隊列中清除。

Timer類的cancel()方法

作用是任務隊列中全部任務清空。

注意事項:

Timer類中的cancel()方法有時并不一定會停止執(zhí)行計劃任務,而是正常執(zhí)行。

下面是一個例子:

public class TimerCancelTest {
    static int i = 0;
    static public class MyTask extends TimerTask {
        @Override
        public void run() {
            System.out.println("正常執(zhí)行了:i= " + i +  " 運行時間為:" + new Date().toLocaleString());
        }
    }

    public static void main(String[] args){
        while (true){
            try {
                i++;
                Timer timer = new Timer();
                MyTask task = new MyTask();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String dateString = "2019-01-21 10:14:10";
                Date dateRef = sdf.parse(dateString);
                timer.schedule(task,dateRef);
                timer.cancel();
            }catch (ParseException e){
                e.printStackTrace();
            }
        }
    }
}

運行結果:

image-20190121105820341

并不是每個任務都被清空了,這是因為Timer類中的cancel()方法并沒有爭搶到queue鎖,所以TimerTask類中的任務繼續(xù)正常執(zhí)行。

方法schedule(TimerTask task,long delay,long period)的使用

作用是以相對時間執(zhí)行定時任務。

同上,可以是使用schedule(TimerTask task,long delay)方法。

方法scheduleAtFixedRate(TimerTask task,Date firstTime,long period)的使用

方法schedule()scheduleAtFixedRate()都會按順序執(zhí)行,所以不要考慮非線程安全的情況。

方法schedule()scheduleAtFixedRate()主要的區(qū)別只在于不延時的情況。

schedule():如果執(zhí)行任務的時間沒有被延時,那么下一次任務的執(zhí)行時間參考的是上一次任務的“開始”時的時間來計算。

scheduleAtFixedRate():如果執(zhí)行任務的時間沒有被延時,那么下一次任務的執(zhí)行時間參考的是上一次任務的“結束”時的時間來計算。

schedule方法不具有追趕執(zhí)行性

錯過的Task循環(huán)任務,就當無事發(fā)生,不執(zhí)行了,這就是Task任務不追趕的情況。

scheduleAtFixedRate方法具有追趕執(zhí)行性

錯過的Task循環(huán)任務將被“補充性”執(zhí)行也就是直接運行錯過任務的次數(shù)。

第六章 單例模式與多線程

立即加載/“餓漢模式”

立即加載就是使用類的時候已經(jīng)將對象創(chuàng)建完畢,常見的實現(xiàn)辦法是直接new實例化。而立即加載從中文的語境來看,有“著急”、“急迫”的含義,所以也稱為“餓漢模式”。

立即加載/“餓漢模式”是在調(diào)用方法前,實例以及被創(chuàng)建了。來看一下實現(xiàn)代碼。

public class MyObject {
    private static MyObject myObject = new MyObject();
    private MyObject(){
        
    }
    public static MyObject getInstance(){
        //此版本為立即加載
        //缺點是不能有其他實例變量
        //因為getInstance()方法沒有同步
        //所以有可能出現(xiàn)非線程安全問題
        return myObject;
    }
}

創(chuàng)建線程類如下

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

創(chuàng)建運行類Run代碼如下

public class Run {
    public static void main(String[] args){
        MyThread t1 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

運行結果
image-20190121140610292

實現(xiàn)了立即加載型單例設計模式。

延遲加載/“懶漢模式”

延遲加載就是在調(diào)用get()方法時實例才被創(chuàng)建,常見的實現(xiàn)辦法就是在get()方法中進行new實例化。而延遲加載從中文語境來看,是“緩慢”、“不急迫”的含義,所以也被稱為“懶漢模式”。

一個簡單實現(xiàn)代碼如下

public class MyDelayObject {
    private static MyDelayObject myObject;
    private MyDelayObject(){
    }
    public static MyDelayObject getInstance(){
        if (myObject == null){
            myObject = new MyDelayObject();
        }
        return myObject;
    }
}

單線程雖然完成了單例,但如果在多線程的環(huán)境中,就會出現(xiàn)取出多個實例的情況。

缺點

多線程情況容易創(chuàng)建多個對象。

public class MyDelayObject {
    private static MyDelayObject myObject = new MyDelayObject();
    private MyDelayObject(){
    }
    public static MyDelayObject getInstance(){
        if (myObject == null){
            //模擬在創(chuàng)建對象之前做一些準備行的工作
            Thread.sleep(3000);
            myObject = new MyDelayObject();
        }
        return myObject;
    }
}

運行結果

image-20190121142623730

返回了不同的對象。

如何解決呢?

1.聲明synchronized

get()添加synchronized關鍵字。

public class MyDelayObject {
    private static MyDelayObject myObject;
    private MyDelayObject(){
    }
    synchronized public static MyDelayObject getInstance(){
        if (myObject == null){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myObject = new MyDelayObject();
        }
        return myObject;
    }
}

運行結果

[圖片上傳失敗...(image-65776b-1548063380608)]

問題解決了,但此種方法效率非常低下,是同步運行的。

2.嘗試同步代碼塊

public class MyDelayObject {
    private static MyDelayObject myObject;
    private MyDelayObject(){
    }
    public static MyDelayObject getInstance(){
       synchronized (MyDelayObject.class){
           if (myObject == null){
               try {
                   Thread.sleep(3000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               myObject = new MyDelayObject();
           }
       }
        return myObject;
    }
}

同步代碼塊的效果與聲明synchronized關鍵字相同,問題可以解決,但效率低下。

3.使用DCL雙檢查鎖機制

public class MyDelayObject {
    private static MyDelayObject myObject;
    private MyDelayObject(){
    }
    public static MyDelayObject getInstance(){
     try {
         //第一次檢查
         if (myObject == null){
             Thread.sleep(3000);
             //同步部分代碼塊
             synchronized (MyDelayObject.class){
                 //第二次檢查
                 if (myObject == null){
                     myObject = new MyDelayObject();
                 }
             }
     }
     }catch (InterruptedException e){
         e.printStackTrace();
     }
     return myObject;
    }
}

使用雙重檢查鎖功能,成功的解決了“懶漢模式“遇到的多線程的問題。DCL也是大多數(shù)多線程結合單例模式使用的解決方案。

使用靜態(tài)內(nèi)置類實現(xiàn)單例模式

使用了這么一個特性:加載一個類時,其內(nèi)部類不會同時被加載。一個類被加載,當且僅當其某個靜態(tài)成員(靜態(tài)域、構造器、靜態(tài)方法等)被調(diào)用時發(fā)生。

public class MyInnerObject {
    private static class MyObjectHandler{
        private static MyInnerObject myInnerObject = new MyInnerObject();
    }
    private MyInnerObject(){}
    public static MyInnerObject getInstance(){
        return MyObjectHandler.myInnerObject;
    }
}

序列化與反序列化的單例模式實現(xiàn)

靜態(tài)內(nèi)置類可以達到線程安全問題,但如果遇到序列化對象時,使用默認的方式運行得到的還是多例。

需要使用一個readResolve()方法

使用static代碼塊實現(xiàn)單例模式

靜態(tài)代碼塊中的代碼在使用類的時候就已經(jīng)執(zhí)行了,所以可以應用這個特性來實現(xiàn)單例模式。

public class MyObject {
    private static MyObject myObject;
    
    static {
        myObject = new MyObject();
    }
    private MyObject(){

    }
    public static MyObject getInstance(){
        return myObject;
    }
}

使用enum枚舉數(shù)據(jù)類型實現(xiàn)單例模式

枚舉enum和靜態(tài)代碼塊的特性相似,在使用枚舉類時,構造方法會被自動調(diào)用。

第七章 拾遺增補

SimpleDateFormat非線程安全

SimpleDateFormat類主要負責日期的轉(zhuǎn)換與格式化,但在多線程的環(huán)境中,使用此類容易造成數(shù)據(jù)轉(zhuǎn)換及處理的不準確,因為SimpleDateFormat并不是線程安全的。

以下是一個例子

public class MyThread extends Thread {
    private SimpleDateFormat sdf;
    private String dateString;

    public MyThread(SimpleDateFormat sdf, String dateString) {
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date dateRef = sdf.parse(dateString);
            String newDateString = sdf.format(dateRef).toString();
            if (!newDateString.equals(dateString)){
                System.out.println("報錯了 日期字符串: " + dateString + " 轉(zhuǎn)換后的日期為: " + newDateString);
            }
        }catch (ParseException e){
            e.printStackTrace();
        }
    }
}
public class Run {
    public static void main(String[] args){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] dateStringArray = new String[]{"2000-01-01","2000-01-02","2000-01-03","2000-01-04","2000-01-05","2000-01-06"};
        MyThread[] threads = new MyThread[6];
        for (int i = 0; i < 6; i++) {
            System.out.println(dateStringArray[i]);
            threads[i] = new MyThread(sdf,dateStringArray[i]);
        }
        for (int i = 0; i <6; i++) {
            threads[i].start();
        }
    }
}

運行結果

image-20190121171211813
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,257評論 0 23
  • 最近讀完了《java多線程編程核心技術》(高洪巖)、《Android高性能編程》(葉坤 譯)、《Java REST...
    捉影T_T900閱讀 338評論 0 2
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,989評論 3 53
  • 文章來源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,896評論 0 1
  • 例1.采集了178種意大利葡萄酒的13種化學成分的數(shù)據(jù),試對葡萄酒進行聚類分析。 數(shù)據(jù):rattle包-wine ...
    宴長閱讀 1,406評論 0 3

友情鏈接更多精彩內(nèi)容