文章簡(jiǎn)介
上一篇文章【「阿里面試系列」搞懂并發(fā)編程叁扫,輕松應(yīng)對(duì)80%的面試場(chǎng)景】我們了解了進(jìn)程和線程的發(fā)展歷史地来、線程的生命周期乘寒、線程的優(yōu)勢(shì)和使用場(chǎng)景,這一篇万栅,我們從Java層面更進(jìn)一步了解線程的使用佑钾。關(guān)注我的技術(shù)公眾號(hào)【架構(gòu)師修煉寶典】一周出產(chǎn)1-2篇技術(shù)文章。Q群725219329分享并發(fā)編程烦粒,分布式休溶,微服務(wù)架構(gòu),性能優(yōu)化扰她,源碼兽掰,設(shè)計(jì)模式,高并發(fā)义黎,高可用禾进,Spring,Netty廉涕,tomcat泻云,JVM等技術(shù)視頻艇拍。
內(nèi)容導(dǎo)航
- 并發(fā)編程的挑戰(zhàn)
- 線程在Java中的使用
并發(fā)編程的挑戰(zhàn)
引入多線程的目的在第一篇提到過,就是為了充分利用CPU是的程序運(yùn)行得更快宠纯,當(dāng)然并不是說啟動(dòng)的線程越多越好卸夕。在實(shí)際使用多線程的時(shí)候,會(huì)面臨非常多的挑戰(zhàn)
線程安全問題
線程安全問題值的是當(dāng)多個(gè)線程訪問同一個(gè)對(duì)象時(shí)婆瓜,如果不考慮這些運(yùn)行時(shí)環(huán)境采用的調(diào)度方式或者這些線程將如何交替執(zhí)行快集,并且在代碼中不需要任何同步操作的情況下,這個(gè)類都能夠表現(xiàn)出正確的行為廉白,那么這個(gè)類就是線程安全的
比如下面的代碼是一個(gè)單例模式个初,在代碼的注釋出,如果多個(gè)線程并發(fā)訪問猴蹂,則會(huì)出現(xiàn)多個(gè)實(shí)例院溺。導(dǎo)致無法實(shí)現(xiàn)單例的效果
public class SingletonDemo {
private static SingletonDemo singletonDemo=null;
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(singletonDemo==null){/***線程安全問題***/
singletonDemo=new SingletonDemo();
}
return singletonDemo;
}
}
通常來說,我們把多線程編程中的線程安全問題歸類成如下三個(gè)磅轻,至于每一個(gè)問題的本質(zhì)珍逸,在后續(xù)的文章中我們會(huì)單獨(dú)講解
- 原子性
- 可見性
- 有序性
上下文切換問題
在單核心CPU架構(gòu)中,對(duì)于多線程的運(yùn)行是基于CPU時(shí)間片切換來實(shí)現(xiàn)的偽并行聋溜。由于時(shí)間片非常短導(dǎo)致用戶以為是多個(gè)線程并行執(zhí)行谆膳。而一次上下文切換,實(shí)際就是當(dāng)前線程執(zhí)行一個(gè)時(shí)間片之后切換到另外一個(gè)線程撮躁,并且保存當(dāng)前線程執(zhí)行的狀態(tài)這個(gè)過程漱病。上下文切換會(huì)影響到線程的執(zhí)行速度,對(duì)于系統(tǒng)來說意味著會(huì)消耗大量的CPU時(shí)間
減少上下文切換的方式
- 無鎖并發(fā)編程馒胆,在多線程競(jìng)爭(zhēng)鎖時(shí)缨称,會(huì)導(dǎo)致大量的上下文切換凝果。避免使用鎖去解決并發(fā)問題可以減少上下文切換
- CAS算法祝迂,CAS是一種樂觀鎖機(jī)制,不需要加鎖
- 使用與硬件資源匹配合適的線程數(shù)
死鎖
在解決線程安全問題的場(chǎng)景中器净,我們會(huì)比較多的考慮使用鎖型雳,因?yàn)樗褂帽容^簡(jiǎn)單。但是鎖的使用如果不恰當(dāng)山害,則會(huì)引發(fā)死鎖的可能性纠俭,一旦產(chǎn)生死鎖,就會(huì)造成比較嚴(yán)重的問題:產(chǎn)生死鎖的線程會(huì)一直占用鎖資源浪慌,導(dǎo)致其他嘗試獲取鎖的線程也發(fā)生死鎖冤荆,造成系統(tǒng)崩潰
以下是死鎖的簡(jiǎn)單案例
public class DeadLockDemo {
//定義鎖對(duì)象
private final Object lockA = new Object();
private final Object lockB = new Object();
private void deadLock(){
new Thread(()->{
synchronized (lockA){
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("Lock B");
}
}
}).start();
new Thread(()->{
synchronized (lockB){
synchronized (lockA){
System.out.println("Lock A");
}
}
}).start();
}
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
}
通過jstack分析死鎖
1.首先通過jps獲取當(dāng)前運(yùn)行的進(jìn)程的pid
6628 Jps
17588 RemoteMavenServer
19220 Launcher
19004 DeadLockDemo
2.jstack打印堆棧信息,輸入 jstack19004, 會(huì)打印如下日志,可以很明顯看到死鎖的信息提示
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001d461e68 (object 0x000000076b310df8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001d463258 (object 0x000000076b310e08, a java.lang.Object),
which is held by "Thread-1"
解決死鎖的手段
1.保證多個(gè)線程按照相同的順序獲取鎖
2.設(shè)置獲取鎖的超時(shí)時(shí)間权纤,超過設(shè)定時(shí)間以后自動(dòng)釋放
3.死鎖檢測(cè)
資源限制
資源限制主要指的是硬件資源和軟件資源钓简,在開發(fā)多線程應(yīng)用時(shí)乌妒,程序的執(zhí)行速度受限于這兩個(gè)資源。硬件的資源限制無非就是磁盤外邓、CPU撤蚊、內(nèi)存、網(wǎng)絡(luò)损话;軟件資源的限制有很多侦啸,比如數(shù)據(jù)庫連接數(shù)、計(jì)算機(jī)能夠支持的最大連接數(shù)等
資源限制導(dǎo)致的問題最直觀的體現(xiàn)就是前面說的上下文切換丧枪,也就是CPU資源和線程資源的嚴(yán)重不均衡導(dǎo)致頻繁上下文切換光涂,反而會(huì)造成程序的運(yùn)行速度下降
資源限制的主要解決方案,就是缺啥補(bǔ)啥拧烦。CPU不夠用顶捷,可以增加CPU核心數(shù);一臺(tái)機(jī)器的資源有限屎篱,則增加多臺(tái)機(jī)器來做集群服赎。
線程在Java中的使用
在Java中實(shí)現(xiàn)多線程的方式比較簡(jiǎn)單,因?yàn)镴ava中提供了非常方便的API來實(shí)現(xiàn)多線程交播。
1.繼承Thread類實(shí)現(xiàn)多線程
2.實(shí)現(xiàn)Runnable接口
3.實(shí)現(xiàn)Callable接口通過Future包裝器來創(chuàng)建Thread線程重虑,這種是帶返回值的線程
4.使用線程池ExecutorService
關(guān)注我的技術(shù)公眾號(hào)【架構(gòu)師修煉寶典】一周出產(chǎn)1-2篇技術(shù)文章。Q群725219329分享并發(fā)編程秦士,分布式缺厉,微服務(wù)架構(gòu),性能優(yōu)化隧土,源碼提针,設(shè)計(jì)模式,高并發(fā)曹傀,高可用辐脖,Spring,Netty皆愉,tomcat嗜价,JVM等技術(shù)視頻。
繼承Thread類
繼承Thread類幕庐,然后重寫run方法久锥,在run方法中編寫當(dāng)前線程需要執(zhí)行的邏輯。最后通過線程實(shí)例的start方法來啟動(dòng)一個(gè)線程
public class ThreadDemo extends Thread{
@Override
public void run() {
//重寫run方法异剥,提供當(dāng)前線程執(zhí)行的邏輯
System.out.println("Hello world");
}
public static void main(String[] args) {
ThreadDemo threadDemo=new ThreadDemo();
threadDemo.start();
}
}
Thread類其實(shí)是實(shí)現(xiàn)了Runnable接口瑟由,因此Thread自己也是一個(gè)線程實(shí)例,但是我們不能直接用 newThread().start()去啟動(dòng)一個(gè)線程冤寿,原因很簡(jiǎn)單歹苦,Thread類中的run方法是沒有實(shí)際意義的绿鸣,只是一個(gè)調(diào)用通過構(gòu)造函數(shù)傳遞寄來的另一個(gè)Runnable實(shí)現(xiàn)類的run方法,這塊的具體演示會(huì)在Runnable接口的代碼中看到
public
class Thread implements Runnable {
/* What will be run. */
private Runnable target;
...
@Override
public void run() {
if (target != null) {
target.run();
}
}
...
實(shí)現(xiàn)Runnable接口
如果需要使用線程的類已經(jīng)繼承了其他的類暂氯,那么按照J(rèn)ava的單一繼承原則潮模,無法再繼承Thread類來實(shí)現(xiàn)線程,所以可以通過實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)多線程
public class RunnableDemo implements Runnable{
@Override
public void run() {
//重寫run方法痴施,提供當(dāng)前線程執(zhí)行的邏輯
System.out.println("Hello world");
}
public static void main(String[] args) {
RunnableDemo runnableDemo=new RunnableDemo();
new Thread(runnableDemo).start();
}
}
上面的代碼中擎厢,實(shí)現(xiàn)了Runnable接口,重寫了run方法辣吃;接著為了能夠啟動(dòng)RunnableDemo這個(gè)線程动遭,必須要實(shí)例化一個(gè)Thread類,通過構(gòu)造方法傳遞一個(gè)Runnable接口實(shí)現(xiàn)類去啟動(dòng)神得,Thread的run方法就會(huì)調(diào)用target.run來運(yùn)行當(dāng)前線程,代碼在上面.
實(shí)現(xiàn)Callable接口
在有些多線程使用的場(chǎng)景中厘惦,我們有時(shí)候需要獲取異步線程執(zhí)行完畢以后的反饋結(jié)果,也許是主線程需要拿到子線程的執(zhí)行結(jié)果來處理其他業(yè)務(wù)邏輯哩簿,也許是需要知道線程執(zhí)行的狀態(tài)宵蕉。那么Callable接口可以很好的實(shí)現(xiàn)這個(gè)功能
public class CallableDemo implements Callable<String>{
@Override
public String call() throws Exception {
return "hello world";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable=new CallableDemo();
FutureTask<String> task=new FutureTask<>(callable);
new Thread(task).start();
System.out.println(task.get());//獲取線程的返回值
}
}
在上面代碼案例中的最后一行 task.get()就是獲取線程的返回值,這個(gè)過程是阻塞的节榜,當(dāng)子線程還沒有執(zhí)行完的時(shí)候羡玛,主線程會(huì)一直阻塞直到結(jié)果返回
使用線程池
為了減少頻繁創(chuàng)建線程和銷毀線程帶來的性能開銷,在實(shí)際使用的時(shí)候我們會(huì)采用線程池來創(chuàng)建線程宗苍,在這里我不打算展開多線程的好處和原理稼稿,我會(huì)在后續(xù)的文章中單獨(dú)說明。
public class ExecutorServiceDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//創(chuàng)建一個(gè)固定線程數(shù)的線程池
ExecutorService pool = Executors.newFixedThreadPool(1);
Future future=pool.submit(new CallableDemo());
System.out.println(future.get());
}
}
pool.submit有幾個(gè)重載方法讳窟,可以傳遞帶返回值的線程實(shí)例让歼,也可以傳遞不帶返回值的線程實(shí)例,源代碼如下
/*01*/Future<?> submit(Runnable task);
/*02*/<T> Future<T> submit(Runnable task, T result);
/*03*/<T> Future<T> submit(Callable<T> task);
關(guān)注我的技術(shù)公眾號(hào)【架構(gòu)師修煉寶典】一周出產(chǎn)1-2篇技術(shù)文章丽啡。Q群725219329分享并發(fā)編程谋右,分布式,微服務(wù)架構(gòu)碌上,性能優(yōu)化倚评,源碼,設(shè)計(jì)模式馏予,高并發(fā),高可用盔性,Spring霞丧,Netty,tomcat冕香,JVM等技術(shù)視頻蛹尝。