Thread類的sleep()方法和對(duì)象的wait()方法都可以讓線程暫停執(zhí)行,它們有什么區(qū)別?
sleep()
方法(休眠)是線程類(Thread)的靜態(tài)方法网梢,調(diào)用此方法會(huì)讓當(dāng)前線程暫停執(zhí)行指定的時(shí)間震缭,將執(zhí)行機(jī)會(huì)(CPU)讓給其他線程,但是對(duì)象的鎖依然保持战虏,因此休眠時(shí)間結(jié)束后會(huì)自動(dòng)恢復(fù)(線程回到就緒狀態(tài)拣宰,請(qǐng)參考第66題中的線程狀態(tài)轉(zhuǎn)換圖)。
wait()
是Object類的方法巡社,調(diào)用對(duì)象的wait()方法導(dǎo)致當(dāng)前線程放棄對(duì)象的鎖(線程暫停執(zhí)行),進(jìn)入對(duì)象的等待池(wait pool)手趣,只有調(diào)用對(duì)象的notify()方法(或notifyAll()方法)時(shí)才能喚醒等待池中的線程進(jìn)入等鎖池(lock pool)晌该,如果線程重新獲得對(duì)象的鎖就可以進(jìn)入就緒狀態(tài)。
補(bǔ)充:可能不少人對(duì)什么是進(jìn)程回懦,什么是線程還比較模糊气笙,對(duì)于為什么需要多線程編程也不是特別理解。簡單的說:進(jìn)程是具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動(dòng)怯晕,是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位潜圃;線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位舟茶,是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位谭期。線程的劃分尺度小于進(jìn)程堵第,這使得多線程程序的并發(fā)性高;進(jìn)程在執(zhí)行時(shí)通常擁有獨(dú)立的內(nèi)存單元隧出,而線程之間可以共享內(nèi)存踏志。使用多線程的編程通常能夠帶來更好的性能和用戶體驗(yàn),但是多線程的程序?qū)τ谄渌绦蚴遣挥押玫恼偷桑驗(yàn)樗赡苷加昧烁嗟腃PU資源针余。當(dāng)然,也不是線程越多凄诞,程序的性能就越好圆雁,因?yàn)榫€程之間的調(diào)度和切換也會(huì)浪費(fèi)CPU時(shí)間。
線程的sleep()方法和yield()方法有什么區(qū)別帆谍?
① sleep()方法給其他線程運(yùn)行機(jī)會(huì)時(shí)不考慮線程的優(yōu)先級(jí)伪朽,因此會(huì)給低優(yōu)先級(jí)的線程以運(yùn)行的機(jī)會(huì);yield()方法只會(huì)給相同優(yōu)先級(jí)或更高優(yōu)先級(jí)的線程以運(yùn)行的機(jī)會(huì)汛蝙;
② 線程執(zhí)行sleep()方法后轉(zhuǎn)入阻塞(blocked)狀態(tài)烈涮,而執(zhí)行yield()方法后轉(zhuǎn)入就緒(ready)狀態(tài);
③ sleep()方法聲明拋出InterruptedException窖剑,而yield()方法沒有聲明任何異常坚洽;
④ sleep()方法比yield()方法(跟操作系統(tǒng)CPU調(diào)度相關(guān))具有更好的可移植性。
JVM中線程的狀態(tài)轉(zhuǎn)換圖
1苛吱、新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象酪术。
2、就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后翠储,其他線程調(diào)用了該對(duì)象的start()方法绘雁。該狀態(tài)的線程位于“可運(yùn)行線程池”中,變得可運(yùn)行援所,只等待獲取CPU的使用權(quán)庐舟。即在就緒狀態(tài)的進(jìn)程除 CPU之外,其它的運(yùn)行所需資源都已全部獲得住拭。
3挪略、運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼滔岳。
4杠娱、阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行谱煤。直到線程進(jìn)入就緒狀態(tài)摊求,才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。
阻塞的情況分三種:
- 等待阻塞:運(yùn)行的線程執(zhí)行wait()方法刘离,該線程會(huì)釋放占用的所有資源室叉,JVM會(huì)把該線程放入“等待池”中睹栖。進(jìn)入這個(gè)狀態(tài)后,是不能自動(dòng)喚醒的茧痕,必須依靠其他線程調(diào)用notify()或notifyAll()方法才能被喚醒野来,
- 同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用踪旷,則JVM會(huì)把該線程放入“鎖池”中曼氛。
- 其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí)埃脏,JVM會(huì)把該線程置為阻塞狀態(tài)搪锣。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)彩掐、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)灰追。
5堵幽、死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期弹澎。
當(dāng)一個(gè)線程進(jìn)入一個(gè)對(duì)象的synchronized方法A之后朴下,其它線程是否可進(jìn)入此對(duì)象的synchronized方法B?
不能苦蒿。其它線程只能訪問該對(duì)象的非同步方法殴胧,同步方法則不能進(jìn)入。因?yàn)榉庆o態(tài)方法上的synchronized修飾符要求執(zhí)行方法時(shí)要獲得對(duì)象的鎖佩迟,如果已經(jīng)進(jìn)入A方法說明對(duì)象鎖已經(jīng)被取走团滥,那么試圖進(jìn)入B方法的線程就只能在等鎖池(注意不是等待池哦)中等待對(duì)象的鎖。
請(qǐng)說出與線程同步以及線程調(diào)度相關(guān)的方法
- wait():使一個(gè)線程處于等待(阻塞)狀態(tài)报强,并且釋放所持有的對(duì)象的鎖灸姊;
- sleep():使一個(gè)正在運(yùn)行的線程處于睡眠狀態(tài),是一個(gè)靜態(tài)方法秉溉,調(diào)用此方法要處理InterruptedException異常力惯;
- notify():喚醒一個(gè)處于等待狀態(tài)的線程,當(dāng)然在調(diào)用此方法的時(shí)候召嘶,并不能確切的喚醒某一個(gè)等待狀態(tài)的線程父晶,而是由JVM確定喚醒哪個(gè)線程,而且與優(yōu)先級(jí)無關(guān)弄跌;
- notityAll():喚醒所有處于等待狀態(tài)的線程甲喝,該方法并不是將對(duì)象的鎖給所有線程,而是讓它們競爭碟绑,只有獲得鎖的線程才能進(jìn)入就緒狀態(tài)俺猿;
補(bǔ)充:Java 5通過Lock接口提供了顯式的鎖機(jī)制(explicit lock)茎匠,增強(qiáng)了靈活性以及對(duì)線程的協(xié)調(diào)。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法押袍,同時(shí)還提供了newCondition()方法來產(chǎn)生用于線程之間通信的Condition對(duì)象诵冒;此外,Java 5還提供了信號(hào)量機(jī)制(semaphore)谊惭,信號(hào)量可以用來限制對(duì)某個(gè)共享資源進(jìn)行訪問的線程的數(shù)量汽馋。在對(duì)資源進(jìn)行訪問之前,線程必須得到信號(hào)量的許可(調(diào)用Semaphore對(duì)象的acquire()方法)圈盔;在完成對(duì)資源的訪問后豹芯,線程必須向信號(hào)量歸還許可(調(diào)用Semaphore對(duì)象的release()方法)。
eg : 下面的例子演示了100個(gè)線程同時(shí)向一個(gè)銀行賬戶中存入1元錢驱敲,在沒有使用同步機(jī)制和使用同步機(jī)制情況下的執(zhí)行情況:
銀行賬戶類:
/**
* 銀行賬戶
*
*/
public class Account {
private double balance; // 賬戶余額
/**
* 存款
* @param money 存入金額
*/
public void deposit(double money) {
double newBalance = balance + money;
try {
Thread.sleep(10); // 模擬此業(yè)務(wù)需要一段處理時(shí)間
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
/**
* 獲得賬戶余額
*/
public double getBalance() {
return balance;
}
}
存錢線程類:
/**
* 存錢線程
*
*/
public class AddMoneyThread implements Runnable {
private Account account; // 存入賬戶
private double money; // 存入金額
public AddMoneyThread(Account account, double money) {
this.account = account;
this.money = money;
}
@Override
public void run() {
account.deposit(money);
}
}
測試類:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test01 {
public static void main(String[] args) {
Account account = new Account();
ExecutorService service = Executors.newFixedThreadPool(100);
for(int i = 1; i <= 100; i++) {
service.execute(new AddMoneyThread(account, 1));
}
service.shutdown();
while(!service.isTerminated()) {}
System.out.println("賬戶余額: " + account.getBalance());
}
}
在沒有同步的情況下铁蹈,執(zhí)行結(jié)果通常是顯示賬戶余額在10元以下,出現(xiàn)這種狀況的原因是众眨,當(dāng)一個(gè)線程A試圖存入1元的時(shí)候握牧,另外一個(gè)線程B也能夠進(jìn)入存款的方法中,線程B讀取到的賬戶余額仍然是線程A存入1元錢之前的賬戶余額娩梨,因此也是在原來的余額0上面做了加1元的操作沿腰,同理線程C也會(huì)做類似的事情,所以最后100個(gè)線程執(zhí)行結(jié)束時(shí)狈定,本來期望賬戶余額為100元颂龙,但實(shí)際得到的通常在10元以下(很可能是1元哦)。解決這個(gè)問題的辦法就是同步纽什,當(dāng)一個(gè)線程對(duì)銀行賬戶存錢時(shí)措嵌,需要將此賬戶鎖定,待其操作完成后才允許其他的線程進(jìn)行操作稿湿,代碼有如下幾種調(diào)整方案:
在銀行賬戶的存款(deposit)方法上同步(synchronized)關(guān)鍵字:
/**
* 銀行賬戶
*
*/
public class Account {
private double balance; // 賬戶余額
/**
* 存款
* @param money 存入金額
*/
public synchronized void deposit(double money) {
double newBalance = balance + money;
try {
Thread.sleep(10); // 模擬此業(yè)務(wù)需要一段處理時(shí)間
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
/**
* 獲得賬戶余額
*/
public double getBalance() {
return balance;
}
}
在線程調(diào)用存款方法時(shí)對(duì)銀行賬戶進(jìn)行同步:
/**
* 存錢線程
*
*/
public class AddMoneyThread implements Runnable {
private Account account; // 存入賬戶
private double money; // 存入金額
public AddMoneyThread(Account account, double money) {
this.account = account;
this.money = money;
}
@Override
public void run() {
synchronized (account) {
account.deposit(money);
}
}
}
通過Java 5顯示的鎖機(jī)制铅匹,為每個(gè)銀行賬戶創(chuàng)建一個(gè)鎖對(duì)象,在存款操作進(jìn)行加鎖和解鎖的操作:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 銀行賬戶
*
*/
public class Account {
private Lock accountLock = new ReentrantLock();
private double balance; // 賬戶余額
/**
* 存款
*
* @param money
* 存入金額
*/
public void deposit(double money) {
accountLock.lock();
try {
double newBalance = balance + money;
try {
Thread.sleep(10); // 模擬此業(yè)務(wù)需要一段處理時(shí)間
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
balance = newBalance;
}
finally {
accountLock.unlock();
}
}
/**
* 獲得賬戶余額
*/
public double getBalance() {
return balance;
}
}
按照上述三種方式對(duì)代碼進(jìn)行修改后饺藤,重寫執(zhí)行測試代碼Test01包斑,將看到最終的賬戶余額為100元。當(dāng)然也可以使用Semaphore或CountdownLatch來實(shí)現(xiàn)同步涕俗。
編寫多線程程序有幾種實(shí)現(xiàn)方式罗丰?
Java 5以前實(shí)現(xiàn)多線程有兩種實(shí)現(xiàn)方法:一種是繼承Thread類;另一種是實(shí)現(xiàn)Runnable接口再姑。兩種方式都要通過重寫run()方法來定義線程的行為萌抵,推薦使用后者,因?yàn)镴ava中的繼承是單繼承,一個(gè)類有一個(gè)父類绍填,如果繼承了Thread類就無法再繼承其他類了霎桅,顯然使用Runnable接口更為靈活。
補(bǔ)充:Java 5以后創(chuàng)建線程還有第三種方式:實(shí)現(xiàn)Callable接口讨永,該接口中的call方法可以在線程執(zhí)行結(jié)束時(shí)產(chǎn)生一個(gè)返回值滔驶,代碼如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyTask implements Callable<Integer> {
private int upperBounds;
public MyTask(int upperBounds) {
this.upperBounds = upperBounds;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= upperBounds; i++) {
sum += i;
}
return sum;
}
}
class Test {
public static void main(String[] args) throws Exception {
List<Future<Integer>> list = new ArrayList<>();
ExecutorService service = Executors.newFixedThreadPool(10);
for(int i = 0; i < 10; i++) {
list.add(service.submit(new MyTask((int) (Math.random() * 100))));
}
int sum = 0;
for(Future<Integer> future : list) {
// while(!future.isDone()) ;
sum += future.get();
}
System.out.println(sum);
}
}
synchronized關(guān)鍵字的用法?
synchronized關(guān)鍵字可以將對(duì)象或者方法標(biāo)記為同步卿闹,以實(shí)現(xiàn)對(duì)對(duì)象和方法的互斥訪問揭糕,可以用synchronized(對(duì)象) { … }定義同步代碼塊,或者在聲明方法時(shí)將synchronized作為方法的修飾符锻霎。
public synchronized void deposit(double money) {
...
}
舉例說明同步和異步
如果系統(tǒng)中存在臨界資源(資源數(shù)量少于競爭資源的線程數(shù)量的資源)著角,例如正在寫的數(shù)據(jù)以后可能被另一個(gè)線程讀到,或者正在讀的數(shù)據(jù)可能已經(jīng)被另一個(gè)線程寫過了旋恼,那么這些數(shù)據(jù)就必須進(jìn)行同步存壤艨凇(數(shù)據(jù)庫操作中的排他鎖就是最好的例子)。當(dāng)應(yīng)用程序在對(duì)象上調(diào)用了一個(gè)需要花費(fèi)很長時(shí)間來執(zhí)行的方法冰更,并且不希望讓程序等待方法的返回時(shí)锨侯,就應(yīng)該使用異步編程,在很多情況下采用異步途徑往往更有效率冬殃。事實(shí)上,所謂的同步就是指阻塞式操作叁怪,而異步就是非阻塞式操作审葬。
啟動(dòng)一個(gè)線程是調(diào)用run()還是start()方法?
啟動(dòng)一個(gè)線程是調(diào)用start()方法奕谭,使線程所代表的虛擬處理機(jī)處于可運(yùn)行狀態(tài)涣觉,這意味著它可以由JVM 調(diào)度并執(zhí)行,這并不意味著線程就會(huì)立即運(yùn)行血柳。run()方法是線程啟動(dòng)后要進(jìn)行回調(diào)(callback)的方法官册。
什么是線程池(thread pool)
在面向?qū)ο缶幊讨校瑒?chuàng)建和銷毀對(duì)象是很費(fèi)時(shí)間的难捌,因?yàn)閯?chuàng)建一個(gè)對(duì)象要獲取內(nèi)存資源或者其它更多資源膝宁。在Java中更是如此,虛擬機(jī)將試圖跟蹤每一個(gè)對(duì)象根吁,以便能夠在對(duì)象銷毀后進(jìn)行垃圾回收员淫。所以提高服務(wù)程序效率的一個(gè)手段就是盡可能減少創(chuàng)建和銷毀對(duì)象的次數(shù),特別是一些很耗資源的對(duì)象創(chuàng)建和銷毀击敌,這就是”池化資源”技術(shù)產(chǎn)生的原因介返。線程池顧名思義就是事先創(chuàng)建若干個(gè)可執(zhí)行的線程放入一個(gè)池(容器)中,需要的時(shí)候從池中獲取線程不用自行創(chuàng)建,使用完畢不需要銷毀線程而是放回池中圣蝎,從而減少創(chuàng)建和銷毀線程對(duì)象的開銷刃宵。
Java 5+中的Executor接口定義一個(gè)執(zhí)行線程的工具。它的子類型即線程池接口是ExecutorService徘公。要配置一個(gè)線程池是比較復(fù)雜的牲证,尤其是對(duì)于線程池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態(tài)工廠方法步淹,生成一些常用的線程池从隆,如下所示:
- newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池。這個(gè)線程池只有一個(gè)線程在工作缭裆,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)键闺。如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束,那么會(huì)有一個(gè)新的線程來替代它澈驼。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行辛燥。
- newFixedThreadPool:創(chuàng)建固定大小的線程池。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程缝其,直到線程達(dá)到線程池的最大大小挎塌。線程池的大小一旦達(dá)到最大值就會(huì)保持不變,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束内边,那么線程池會(huì)補(bǔ)充一個(gè)新線程榴都。
- newCachedThreadPool:創(chuàng)建一個(gè)可緩存的線程池。如果線程池的大小超過了處理任務(wù)所需要的線程漠其,那么就會(huì)回收部分空閑(60秒不執(zhí)行任務(wù))的線程嘴高,當(dāng)任務(wù)數(shù)增加時(shí),此線程池又可以智能的添加新線程來處理任務(wù)和屎。此線程池不會(huì)對(duì)線程池大小做限制拴驮,線程池大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小。
- newScheduledThreadPool:創(chuàng)建一個(gè)大小無限的線程池柴信。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求套啤。
- newSingleThreadExecutor:創(chuàng)建一個(gè)單線程的線程池。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求随常。
簡述synchronized 和java.util.concurrent.locks.Lock的異同
Lock是Java 5以后引入的新的API潜沦,和關(guān)鍵字synchronized相比主要相同點(diǎn):Lock 能完成synchronized所實(shí)現(xiàn)的所有功能;主要不同點(diǎn):Lock有比synchronized更精確的線程語義和更好的性能线罕,而且不強(qiáng)制性的要求一定要獲得鎖止潮。synchronized會(huì)自動(dòng)釋放鎖,而Lock一定要求程序員手工釋放钞楼,并且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)喇闸。