一、CPU、進(jìn)程炊汤、線程
CPU
我們都知道CPU是計(jì)算機(jī)的中央處理器,CPU的主要功能是運(yùn)算弊攘。對CPU的關(guān)注通常都是CPU的核心數(shù)和CPU的線程數(shù)抢腐。而CPU的核心數(shù)代表硬件上存在著幾個核心。而這些核心是相對獨(dú)立的CPU核心單元組襟交。在沒有超線程技術(shù)的時(shí)候氓栈,一個核心只能處理一個線程,而有了超線程技術(shù)后婿着,一個核心可以處理兩個線程授瘦。例如雙核心四線程。就是有兩個CPU核心而每個核心可以處理的線程數(shù)就是兩個竟宋。進(jìn)程
進(jìn)程即為程序運(yùn)行的資源分配和調(diào)用的基本單位提完,而進(jìn)程里面是存在多個線程。進(jìn)程中的每個線程都會共享進(jìn)程中的資源丘侠。例如啟動我們的JAVA應(yīng)用就是啟動一個進(jìn)程徒欣,每個請求我們都使用一個線程處理,而每個請求中使用到的全局變量這些就是共享的資源蜗字。線程
線程是CPU執(zhí)行的基本單位打肝。線程可以把進(jìn)程的資源分配和執(zhí)行調(diào)度分開,各個線程既可以共享進(jìn)程資源挪捕,又可以獨(dú)立調(diào)度粗梭。而在同一時(shí)間片刻下CPU只能執(zhí)行1個線程。多線程執(zhí)行CPU通過分配給每個線程的執(zhí)行時(shí)間段以達(dá)到多線程計(jì)算级零。這種切換會導(dǎo)致上下文切換断医。而頻繁的上下文切換會影響性能。而至于怎樣影響可以參考《多線程上下文切換》(http://www.reibang.com/p/19fc8aca712c)
二奏纪、線程調(diào)度
2.1什么是線程調(diào)度
線程調(diào)度就是系統(tǒng)為線程分配執(zhí)行時(shí)間的過程鉴嗤。
2.2 線程調(diào)度的方式
根據(jù)線程調(diào)度的控制權(quán)是由系統(tǒng)控制或者線程本身來控制劃分為:協(xié)同式的線程調(diào)度和搶占式的線程調(diào)度。
1序调、協(xié)同式線程調(diào)度:線程之間的系統(tǒng)執(zhí)行時(shí)間醉锅,由線程本身進(jìn)行進(jìn)行控制。這種線程調(diào)度方式就像接力賽发绢,一個執(zhí)行完畢后交由下一個接力硬耍。如當(dāng)前線程執(zhí)行完畢后垄琐,通知系統(tǒng)調(diào)度到其他線程執(zhí)行。
(1)協(xié)同的好處:線程的切換是可預(yù)知的默垄。線程之間不存在同步的問題此虑。
(2)協(xié)同的壞處:協(xié)同調(diào)度的致命缺點(diǎn)是當(dāng)某個線程執(zhí)行有問題的時(shí)候甚纲,會導(dǎo)致整個運(yùn)行阻塞和系統(tǒng)崩潰口锭。
2、搶占式線程調(diào)度:線程之間的系統(tǒng)執(zhí)行時(shí)間介杆,是由系統(tǒng)進(jìn)行控制鹃操。而搶占式的線程調(diào)度對線程的不可預(yù)知,系統(tǒng)定期的中斷當(dāng)前正在執(zhí)行的線程春哨,將CPU執(zhí)行權(quán)切換到下一個等待的線程荆隘。所以任何一個線程都不能獨(dú)占CPU。正因?yàn)檫@種定期的線程切換導(dǎo)致線程之間存在不同的問題赴背。當(dāng)線程執(zhí)行過程中椰拒,某個線程出現(xiàn)問題的時(shí)候,由線程對CPU不具有獨(dú)占性凰荚。因此不會造成阻塞燃观。
我們所使用的操作系統(tǒng)都是是用搶占性的線程調(diào)度。如果使用協(xié)同式的線程調(diào)度情況下便瑟,如果我們再使用某個軟件出現(xiàn)問題時(shí)候缆毁,操作系統(tǒng)處于阻塞狀態(tài),導(dǎo)致整個操作系統(tǒng)崩潰到涂,我們肯定會抓狂脊框。
3、JAVA線程調(diào)度
Java線程調(diào)度就是搶占式調(diào)度践啄。
三浇雹、Java線程的實(shí)現(xiàn)方式
JAVA提供了3中創(chuàng)建線程的方式:
- Thread
繼承Thread類重寫run方法,這種創(chuàng)建線程的方式在我們的編程中很少使用屿讽。
private static class TheadExtends extends Thread{
@Override
public void run() {
System.out.println("TheadExtends");
}
}
- Runnable
既然JAVA提供了Thread創(chuàng)建線程的方式箫爷,為什么還要提供Runnable接口的方式進(jìn)行創(chuàng)建線程?因?yàn)镴AVA是單繼承聂儒,不能多繼承虎锚。因此就有了Runnable接口的方式來進(jìn)行創(chuàng)建線程。
private static class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println("RunnableImpl");
}
}
- Callable
Callable接口與Runnable接口的區(qū)別在于Callable在線程調(diào)用完畢后有返回結(jié)果衩婚,而Runnable沒有窜护,而對于一些業(yè)務(wù)處理比較耗時(shí)并且無需立即返回處理結(jié)果的情況下,我們都會通過asynchronous+Future的方式處理非春,而對于這種業(yè)務(wù)情景我們可以通過Callable進(jìn)行處理柱徙。
public class ThreadImplement {
private static class CallableImpl implements Callable<String>{
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "Callable";
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableImpl callableImpl=new CallableImpl();
FutureTask<String> futureTask = new FutureTask<String>(callableImpl);
Thread CallableThread= new Thread(futureTask);
CallableThread.start();
System.out.println(futureTask.get());
}
}
四缓屠、JAVA線程狀態(tài)轉(zhuǎn)換
4.1Java線程狀態(tài)轉(zhuǎn)換圖
4.2Java線程狀態(tài)
JAVA線程狀態(tài)包括:
New :新創(chuàng)建一個線程是處于該狀態(tài)。
Runnable:線程的調(diào)度是由操作系統(tǒng)可以決定护侮,因此Runnable是包含Ready和Running敌完。當(dāng)我們調(diào)用了start()方法后,當(dāng)前的線程處于一個Ready的狀態(tài)羊初,等待操作系統(tǒng)線程調(diào)用到當(dāng)前線程分配CPU執(zhí)行時(shí)間滨溉,若當(dāng)前線程獲得CPU執(zhí)行時(shí)間時(shí),線程就處于一個Running的狀態(tài)长赞。而在Running狀態(tài)的情況下晦攒,我們可以調(diào)用yield()方法,放棄當(dāng)前線程的CPU執(zhí)行得哆。而調(diào)用yield后當(dāng)前線程處于一個Ready的狀態(tài)脯颜,這種狀態(tài)下操作系統(tǒng)在線程調(diào)度的時(shí)候分配CPU執(zhí)行時(shí)間給當(dāng)前的線程。
Blocked:阻塞狀態(tài)下代表著當(dāng)前的線程被掛起贩据。而這掛起的原因的線程在等待一個鎖栋操。如我們在一個方法或者代碼塊中使用Synchronized時(shí),同一時(shí)間有2個線程進(jìn)入該方法的時(shí)候饱亮,先獲取到鎖的線程執(zhí)行矾芙。而沒有獲得鎖的線程就處于這種阻塞狀態(tài)。
WAITING:等待狀態(tài)下近尚,當(dāng)前線程不被執(zhí)行和操作系統(tǒng)不會給該線程進(jìn)行線程調(diào)度蠕啄。而當(dāng)前線程處于等待其他線程喚醒。只有被喚醒后戈锻,操作系統(tǒng)才會給該線程進(jìn)行線程調(diào)度歼跟。這種線程的等待的主要作用是為了線程之間的協(xié)作。一般情況下通過Synchronized獲得鎖后格遭,調(diào)用鎖的wait的方法進(jìn)入等待狀態(tài)哈街,而調(diào)用wait方法后,當(dāng)前的線程會釋放鎖拒迅,而另外一個線程獲得鎖后骚秦,通過notifyall()/notify()進(jìn)行喚醒處于等待的線程。
TIMED_WAITING:處于這種有限期的等待的情況下璧微,在限期內(nèi)當(dāng)前線程不會被執(zhí)行和操作系統(tǒng)不會給該線程進(jìn)行線程調(diào)度作箍。在限期過后,操作系統(tǒng)才給該線程進(jìn)行線程調(diào)度前硫。
TERMINATED:該狀態(tài)下線程處于終止胞得,而這種終止引起的原因分為正常的執(zhí)行完畢的終止和非正常情況下的終止,而非正常情況下可能是線程執(zhí)行異骋俚纾或者調(diào)用interrupt()中止線程引起阶剑。
五跃巡、多線程編程
5.1 多線程編程的好處
- 充分利用CPU的資源。
- 加快請求響應(yīng)
- 異步化
5.2 多線程帶來的問題
- 設(shè)計(jì)更復(fù)雜
1.線程之間是共享進(jìn)程資源牧愁,存在資源沖突素邪。
2.線程之間的協(xié)作往往是非常復(fù)雜。若不能正確的使用鎖的機(jī)制猪半,通常會造成數(shù)據(jù)錯誤兔朦,整個業(yè)務(wù)功能出現(xiàn)問題。 - 上下文切換的開銷
- 增加資源消耗办龄,多線程變成是一種空間換時(shí)間的方式烘绽。線程在運(yùn)行的時(shí)候需要從計(jì)算機(jī)里面得到一些資源淋昭。除了CPU俐填,線程還需要一些內(nèi)存來維持它本地的堆棧,若開啟過多的線程時(shí)會導(dǎo)致程序占用過多的內(nèi)存和機(jī)器崩潰翔忽。
七英融、線程基本操作
interrupt
- JAVA提倡通過協(xié)作的方式結(jié)束線程,而不是使用強(qiáng)制停止的方式進(jìn)行結(jié)束線程如stop()歇式,resume(),suspend()已不建議使用驶悟,stop()會導(dǎo)致線程不會正確釋放資源,suspend()容易導(dǎo)致死鎖材失。那么怎樣協(xié)同的方式結(jié)束線程呢痕鳍?就是同過Thread的interrupt()方法進(jìn)行協(xié)作中斷線程。而調(diào)用interrupt方法是在線程中設(shè)置一個中斷的標(biāo)志位龙巨,中斷標(biāo)志默認(rèn)為fals笼呆。被中斷的線程通過循環(huán)的方式監(jiān)聽這個標(biāo)志位確定當(dāng)前線程需要中斷。
public static class SafeEndRunnable implements Runnable{
@Override
public void run() {
System.out.println("flag = "+Thread.currentThread().isInterrupted());
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()+"running");
}
System.out.println(Thread.currentThread().getName()+"is end ,flag = "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
SafeEndRunnable safeEndRunnable = new SafeEndRunnable();
Thread t1 = new Thread(safeEndRunnable);
t1.start();
Thread.sleep(1);
t1.interrupt();
}
輸出:
flag = false
Thread-0running
Thread-0running
Thread-0running
Thread-0running
Thread-0is end ,flag = true
- 相關(guān)方法
方法名 | 方法類型 | Demo | 描述 |
---|---|---|---|
isInterrupted | 對象方法 | Thread.currentThread().isInterrupted() |
判斷當(dāng)前線程是否處于中斷狀態(tài) |
interrupt | 對象方法 | Thread.currentThread().interrupt() |
設(shè)置標(biāo)志位為true |
interrupted | 靜態(tài)方法 | Thread.interrupted() |
判斷當(dāng)前線程是否處于中斷狀態(tài)并且設(shè)置中斷狀態(tài)為false |
- 在進(jìn)行協(xié)作處理線程結(jié)束的時(shí)候清除標(biāo)志位旨别。在我們的被中斷的線程中如果使用到了sleep方法時(shí)诗赌,如果中斷線程調(diào)用時(shí),該線程處于sleep時(shí)秸弛,會拋出InterruptedException铭若,如果使用進(jìn)行try/catch捕捉該異常的時(shí)候會清除標(biāo)志位。所以我們需要再調(diào)用被中斷的線程的 interrupt()方法递览。
public static class SafeEndThread implements Runnable{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()+"running");
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
System.out.println("flag = "+Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
}
}
System.out.println(Thread.currentThread().getName()+"is end ,flag = "+Thread.currentThread().isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
SafeEndThread safeEndThread = new SafeEndThread();
Thread t2 = new Thread(safeEndThread);
t2.start();
Thread.sleep(1);
t2.interrupt();
}
輸出:
Thread-0running
java.lang.InterruptedException: sleep interrupted
flag = false
Thread-0is end ,flag = true
yield
yield的主要作用的是讓出CPU的執(zhí)行時(shí)間叼屠,需要注意的時(shí)候,調(diào)用yield雖然讓出了CPU的執(zhí)行時(shí)間绞铃,但是會參與下一次的CPU執(zhí)行時(shí)間的競爭中镜雨,如果當(dāng)前線程重新獲得CPU執(zhí)行時(shí)間,那么當(dāng)前的線程再次執(zhí)行憎兽。如下:
public static class ThreadYieldRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"running"+i);
Thread.yield();
}
}
}
public static void main(String[] args) {
ThreadYieldRunnable threadYieldRunnable= new ThreadYieldRunnable();
Thread t1 = new Thread(threadYieldRunnable);
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1.yield");
}
輸出:
Thread-0running0
Thread-0running1
t1.yield
Thread-0running2
Thread-0running3
Thread-0running4
Thread-0running5
Thread-0running6
Thread-0running7
Thread-0running8
Thread-0running9
Thread-0running10
Thread-0running11
Thread-0running12
Thread-0running13
Thread-0running14
Thread-0running15
Thread-0running16
Thread-0running17
Thread-0running18
Thread-0running19
六冷离、線程共享
鎖的主要作用是保護(hù)臨界區(qū)資源吵冒,在多線程訪問臨界區(qū)時(shí)互斥。那么在線程訪問共享的資源時(shí)西剥,JAVA提供了以下保存線程之間的線程共享資源痹栖。
Synchronized
- Synchronized的實(shí)現(xiàn)方式
方式 | 鎖對象 | Demo |
---|---|---|
對象同步 | 當(dāng)前對象 | synchronized void demo() |
靜態(tài)同步 | 當(dāng)前類 | static synchronized void demo() |
代碼塊 | 當(dāng)前對象、其他對象瞭空、類 |
Demo demo = new Demo(); synchronized (demo) {} synchronized (this){} synchronized(Demo.class) {}
|
Synchronized的綜述
(1)Synchronized主要作用實(shí)現(xiàn)同步揪阿。而這種同步是通過互斥鎖來保證多線程訪問時(shí)實(shí)現(xiàn)同步。即在同一時(shí)間內(nèi)只有一個線程可以訪問臨界區(qū)的資源咆畏,同時(shí)保證了共享資源的可預(yù)見性和原子性南捂。
(2)Synchronized的使用:可以在方法定義中使用,也可以使用同步代碼塊的形式使用旧找。在使用Synchronized的時(shí)候溺健,盡量使用代碼塊的形式,將同步的操作控制在最小的粒度中钮蛛。如果使用在Synchronized在方法定義中鞭缭,那么該方法中不存在鎖競爭的部分會被同步。如果該方法高并發(fā)情況下魏颓,可能會導(dǎo)致多線程等待從而引起應(yīng)用dump掉岭辣。Synchronized死鎖
死鎖引起的原因是由于兩個線程之間,相互持有對象的鎖和相互等待對象釋放鎖甸饱。在使用Synchronized的時(shí)候不允許出現(xiàn)死鎖的情況沦童。
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1 get locke1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1 get locke2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2 get locke2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2 get locke1");
}
}
});
t1.start();
t2.start();
}
- 避免使用常量池對象作為鎖對象
Java為我們提供了String、Integer叹话、Long常量池偷遗,因此我們再使用這些常量池的對象作為鎖對象的時(shí)候,會存在鎖隱患渣刷。
public class ConstLock implements Runnable {
private Object lock ;
public ConstLock(Object lock) {
super();
this.lock = lock;
}
public void run() {
synchronized (lock) {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"sayHello");
}
}
}
public static void main(String[] args) {
ConstLock constLock1 = new ConstLock("lock");
ConstLock constLock2 = new ConstLock("lock");
Thread t1 = new Thread(constLock1,"Thread1");
Thread t2 = new Thread(constLock2,"Thread2");
t1.start();
t2.start();
}
public static void main(String[] args) {
ConstLock constLock1 = new ConstLock(21);
ConstLock constLock2 = new ConstLock(21);
Thread t1 = new Thread(constLock1,"Thread1");
Thread t2 = new Thread(constLock2,"Thread2");
t1.start();
t2.start();
}
public static void main(String[] args) {
ConstLock constLock1 = new ConstLock(21l);
ConstLock constLock2 = new ConstLock(21l);
Thread t1 = new Thread(constLock1,"Thread1");
Thread t2 = new Thread(constLock2,"Thread2");
t1.start();
t2.start();
}
}
打羽兄住:
Thread1sayHello
Thread1sayHello
Thread1sayHello
volatile關(guān)鍵字
volatile是JAVA中提供的一種輕量級的同步機(jī)制。而這種輕量級的同步機(jī)制是通過線程之間的通訊來保證辅柴。而不是通過鎖的機(jī)制進(jìn)行處理箩溃。因此不會對執(zhí)行的線程造成阻塞。
-
線程通訊過程
image.png volatile主要作用
(1)確保了所有的線程對volatile修飾的變量具有可見性碌嘀。
(2)禁止操作系統(tǒng)指令重排序涣旨,如果變量沒有被volatile表示禁止指令重排優(yōu)化的情況下。操作系統(tǒng)默認(rèn)會對不相關(guān)的執(zhí)行指令進(jìn)行重排序提高執(zhí)行的效率股冗。
public class Reorder {
public static int x = 0;
public static int y = 0;
public static int a = 0;
public static int b = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
a = 1;
x = b;
}) ;
Thread thread2 = new Thread(()->{
b = 1;
y = a;
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("x=" + x + ";y=" + y);
}
}
如沒有禁止指令重排序就會出現(xiàn):x=1;y=0霹陡、x=0;y=1、x=1;y=1、x=0;y=0四種結(jié)果烹棉。
- volatile原子性
1攒霹、原子性表示一個操作或者多個操作的情況下,要么全部執(zhí)行成功浆洗,要么全部執(zhí)行失敗催束。
2、volatile在符合條件以下條件的情況下具有原子性:
(1)對volatile修飾的變量的操作不依賴變量本身伏社,如i++這種復(fù)合操作不具有原子性抠刺。代碼如下:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
(2)確保只有一個線程修改變量的值的情況。
- volatile適用場景
(1)禁止系統(tǒng)重排序的情況
(2)只有一個線程寫摘昌,多個線程讀的情況速妖。
ThreadLocal
ThreadLocal是一個線程本地存儲,而每個線程有自己獨(dú)立的副本聪黎。也就是說每個線程可以存放各自變量到線程本地存儲中罕容,并且線程之間各自訪問各自的線程本地存儲。當(dāng)線程結(jié)束后挺举,線程的本地存儲會被垃圾回收杀赢。如果線程本地存儲中的變量被其他引用到的情況下烘跺,是不會被回收湘纵。我們可以把ThreadLocal看作一個Map<Thread,Object>。
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 1;
};
};
public void startThread() {
for (int i = 0; i < 2; i++) {
Thread t1 = new Thread(new ThreadLocalRunnable(i));
t1.start();
}
}
@Data
@AllArgsConstructor
public static class ThreadLocalRunnable implements Runnable {
private int id;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " id=" + id);
int beforeId = threadLocal.get();
int afterId = beforeId + id;
threadLocal.set(afterId);
System.out.println(Thread.currentThread().getName() + " after id =" + threadLocal.get());
}
}
public static void main(String[] args) {
new UseThreadLocal().startThread();
}
輸出:
Thread-0 id=0
Thread-0 after id =1
Thread-1 id=1
Thread-1 after id =2
參考資料:
《深入理解Java虛擬機(jī)》