第一章 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("運行結束");
}
}
運行結果:

主要是通過創(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í)行。
停止線程
- 使用退出標志使線程正常退出,也就是當
run()方法完成后線程終止。 - 使用
stop()方法強行終止線程,但是不推薦使用,是過期作廢的方法,且會終止正在運行中的線程。 - 使用
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源碼如下:

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

線程優(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é)了。
個人理解:將對象監(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("線程被停止了");
}
}
問題就解決了。
兩張圖幫助理解:
程序的私有堆棧:

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

? volatile的缺點時不支持原子性(整個程序中的所有操作,要么全部完成,要么全部不完成,不可能停滯在中間某個環(huán)節(jié))。個人理解:volatile相當于給變量增加了synchronized。
對比volatile和synchronized
- volatile性能比synchronized好,volatile只能修飾于變量,而synchronized可以修飾方法及代碼塊。
- 多線程訪問volatile不會發(fā)生阻塞,而synchronized會出現(xiàn)阻塞。
- volatile能保證數(shù)據(jù)的可見性,但不能保證原子性;而synchronized可以保證原子性也可以間接保證可見效,因為它會將私有內(nèi)存和公有內(nèi)存中的數(shù)據(jù)做同步。
- 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)存中的工作過程:

- read和load階段:從主工作內(nèi)存復制變量到當前線程工作內(nèi)存
- use和assign階段:執(zhí)行代碼,改變共享變量值
- 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個類:
- PipedInputStream和PipedOutputStream
- 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();
}
}
}
運行結果:

任務雖然執(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();
}
}
}
}
運行結果:

并不是每個任務都被清空了,這是因為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();
}
}
運行結果
實現(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;
}
}
運行結果

返回了不同的對象。
如何解決呢?
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();
}
}
}
運行結果
