Java和Android開發(fā)過程中,我們經(jīng)常會提到進程、線程,什么是進程,什么是線程?
進程與線程
進程定義:進程是程序運行資源分配的最小單位(資源:CPU、內(nèi)存空間、IO),進程與進程之間獨立。
線程定義:CPU調(diào)度的最小單位,必須依賴于進程,一個進程內(nèi),允許有多個線程,線程之間可以共享資源。
并行與并發(fā)
舉個例子,如果有條高速公路 A 上面并排有 8 條車道,那么最大的并行車 輛就是 8 輛此條高速公路 A 同時并排行走的車輛小于等于 8 輛的時候,車輛就可 以并行運行。CPU 也是這個原理,一個 CPU 相當于一個高速公路 A,核心數(shù)或者線 程數(shù)就相當于并排可以通行的車道;而多個 CPU 就相當于并排有多條高速公路,而 每個高速公路并排有多個車道。
并發(fā):指應用能夠交替執(zhí)行不同的任務,比如單 CPU 核心下執(zhí)行多線程并非是 同時執(zhí)行多個任務,如果你開兩個線程執(zhí)行,就是在你幾乎不可能察覺到的速度不 斷去切換這兩個任務,已達到"同時執(zhí)行效果",其實并不是的,只是計算機的速度太 快,我們無法察覺到而已. 并行:指應用能夠同時執(zhí)行不同的任務,例:吃飯的時候可以邊吃飯邊打電話, 這兩件事情可以同時執(zhí)行
兩者區(qū)別:一個是交替執(zhí)行,一個是同時執(zhí)行.
Java多線程可以給程序帶來如下好處
1.充分利用CPU資源
2.加快響應用戶的時間
3.可以使你的代碼模塊化,異步化,簡單化
多線程程序需要注意事項
(1)線程之間的安全性 從前面的章節(jié)中我們都知道,在同一個進程里面的多線程是資源共享的,也就 是都可以訪問同一個內(nèi)存地址當中的一個變量。例如:若每個線程中對全局變量、 靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多 個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
(2)線程之間的死鎖 為了解決線程之間的安全性引入了 Java 的鎖機制,而一不小心就會產(chǎn)生 Java 線程死鎖的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從
而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個饑餓的人,他們必 須共享刀叉并輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。
假如線程 A 獲得了刀,而線程 B 獲得了叉。線程 A 就會進入阻塞狀態(tài)來等待 獲得叉,而線程 B 則阻塞來等待線程 A 所擁有的刀。
(3)線程太多了會將服務器資源耗盡形成死機當機 線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導致消耗完系統(tǒng)內(nèi)存以及 CPU 的“過渡切換”,造成系統(tǒng)的死機,那么我們該如何解決這類問題呢? 某些系統(tǒng)資源是有限的,如文件描述符。多線程程序可能耗盡資源,因為每個 線程都可能希望有一個這樣的資源。如果線程數(shù)相當大,或者某個資源的侯選線 程數(shù)遠遠超過了可用的資源數(shù)則最好使用資源池。一個最好的示例是數(shù)據(jù)庫連接 池。只要線程需要使用一個數(shù)據(jù)庫連接,它就從池中取出一個,使用以后再將它返 回池中。資源池也稱為資源庫。
面試中可能會被問到,Java新啟線程的方式有幾種?
從Java源碼上,我們可以看到Java新啟線程的方式有兩種。
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread.
This subclass should override the run method of class Thread.
An instance of the subclass can then be allocated and started.
For example, a thread that computes primes larger than a stated value could be written as follows:
從Thread源碼注釋上,我們可以看到,Java啟動線程的方式有兩種:
- 繼承Thread類,重寫run()方法
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread{
@Override
public void run() {
System.out.println("啟動線程第一種方法,繼承Thread");
}
}
public static void main(String[] args) {
FirstMethodThread firstMethodThread=new FirstMethodThread();
firstMethodThread.start();
}
}
- 實現(xiàn)Runnable接口,實現(xiàn)run方法
public class StartThreadSecondMethod {
static class PrimeRun implements Runnable{
@Override
public void run() {
System.out.println("啟動線程第二種方法,implements Runnable");
}
}
public static void main(String[] args) {
PrimeRun primeRun=new PrimeRun();
new Thread(primeRun).start();
}
}
Thread類是Java對線程概念的抽象,new Thread(),只是new出一個Thread的實例,還沒有和操作系統(tǒng)中真正的線程掛鉤,只有執(zhí)行了start()方法,才真正意義上啟動了線程。start()方法讓線程進入就緒狀態(tài),等待分配CPU,分到CPU以后才會執(zhí)行run()方法,start()方法不能重復調(diào)用,重復調(diào)用會拋異常??!而 run 方法是業(yè)務邏輯實現(xiàn)的地方,本質(zhì)上和任意一個類的任意一個成員方法并沒有任何區(qū)別,可以重復執(zhí)行,也可以被單獨調(diào)用。
有開始就有結(jié)束,怎么樣才能讓Java里的線程安全停止工作呢?
1.線程自然中止
1).run執(zhí)行完成;
2).拋出一個未處理的異常,導致線程提前結(jié)束
2.stop()
暫停、恢復和停止操作對應在線程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是這些 API 是過期的,也就是不建議使用的。不建議使用的原因主 要有:以 suspend()方法為例,在調(diào)用后,線程不會釋放已經(jīng)占有的資源(比如 鎖),而是占有著資源進入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。同樣,stop()方 法在終結(jié)一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資 源釋放工作的機會,因此會導致程序可能工作在不確定狀態(tài)下。正因為 suspend()、 resume()和 stop()方法帶來的副作用,這些方法才被標注為不建議使用的過期方法。
3.interrupt()
安全的中止則是其他線程通過調(diào)用某個線程 A 的 interrupt()方法對其進行中 斷操作, 中斷好比其他線程對該線程打了個招呼,“A,你要中斷了”,不代表 線程 A 會立即停止自己的工作,同樣的 A 線程完全可以不理會這種中斷請求。 因為 java 里的線程是協(xié)作式的,不是搶占式的。線程通過檢查自身的中斷標志 位是否被置為 true 來進行響應。
線程通過方法 isInterrupted()來進行判斷是否被中斷,也可以調(diào)用靜態(tài)方法 Thread.interrupted()來進行判斷當前線程是否被中斷,不過 Thread.interrupted() 會同時將中斷標識位改寫為 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " interrupte flag = " + isInterrupted());
while (!isInterrupted()) {
System.out.println(name + " is runing");
System.out.println(name + " inner interrupte flag = " + isInterrupted());
}
System.out.println(name + " end interrupte flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(30);
firstMethodThread.interrupt();
}
}
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = true
FirstMethodThread end interrupte flag = true
從輸出結(jié)果可以看到,調(diào)用isInterrupted()方法,未改變中斷標識位。
調(diào)用Thread.interrupted()方法,我們可以看到中斷標識位改寫為 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " interrupte flag = " + isInterrupted());
while (!Thread.interrupted()) {
System.out.println(name + " is runing");
System.out.println(name + " inner interrupte flag = " + isInterrupted());
}
System.out.println(name + " end interrupte flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(5);
firstMethodThread.interrupt();
}
}
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = true
FirstMethodThread end interrupte flag = false
如果一個線程處于了阻塞狀態(tài)(如線程調(diào)用了 thread.sleep、thread.join、 thread.wait 等),則在線程在檢查中斷標示時如果發(fā)現(xiàn)中斷標示為 true,則會在 這些阻塞方法調(diào)用處拋出 InterruptedException 異常,并且在拋出異常后會立即 將線程的中斷標示位清除,即重新設置為 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String threadName = currentThread().getName();
System.out.println("thread name is "+threadName);
while (!isInterrupted()){
try {
sleep(100);
System.out.println(threadName +" is running");
} catch (InterruptedException e) {
e.printStackTrace();
// interrupt();
System.out.println(threadName +" isInterrupted is "+isInterrupted());
}
}
System.out.println(threadName +" end isInterrupted is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(1000);
firstMethodThread.interrupt();
}
}
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.java.base.thread.StartThreadFirstMethod$FirstMethodThread.run(StartThreadFirstMethod.java:15)
FirstMethodThread isInterrupted is false
FirstMethodThread is running
從輸出結(jié)果,可以看到,雖然調(diào)用了線程的interrupt方法,但是線程中斷標識位打印出來仍然是false,這是因為在拋出中斷異常的異常時,中斷標識也同時被修改成了false,所以線程不會中止,仍然會一直輸出內(nèi)容,只有在捕獲異常的位置再次調(diào)用interrupt方法,線程才會被真正的中斷。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String threadName = currentThread().getName();
System.out.println("thread name is "+threadName);
while (!isInterrupted()){
try {
sleep(100);
System.out.println(threadName +" is running");
} catch (InterruptedException e) {
e.printStackTrace();
interrupt();
System.out.println(threadName +" isInterrupted is "+isInterrupted());
}
}
System.out.println(threadName +" end isInterrupted is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(1000);
firstMethodThread.interrupt();
}
}
FirstMethodThread is running
FirstMethodThread is running
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.java.base.thread.StartThreadFirstMethod$FirstMethodThread.run(StartThreadFirstMethod.java:15)
FirstMethodThread isInterrupted is true
FirstMethodThread end isInterrupted is true
捕獲異常的位置再次調(diào)用interrupt方法,線程才會被真正的中斷,中斷標識被設置為true.
其他線程相關(guān)方法
1.yield()
使當前CPU讓出CPU占有權(quán),但讓出的時間是不可設定的,也不會釋放鎖資源。線程進入就緒狀態(tài),再次被操作系統(tǒng)選中時又重新開始執(zhí)行。
線程生命周期
2.join()
把指定的線程加入到當前線程,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行。 比如在線程 B 中調(diào)用了線程 A 的 Join()方法,直到線程 A 執(zhí)行完畢后,才會繼續(xù) 執(zhí)行線程 B。
面試考點:如何讓兩個線程順序執(zhí)行
/**
*類說明:演示Join()方法的使用
*/
public class UseJoin {
static class Goddess implements Runnable {
private Thread thread;
public Goddess(Thread thread) {
this.thread = thread;
}
public Goddess() {
}
public void run() {
System.out.println("Goddess開始排隊打飯.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " Goddess打飯完成.");
}
}
static class GoddessLikefriend implements Runnable {
public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("Goddess暗戀目標開始排隊打飯.....");
System.out.println(Thread.currentThread().getName()
+ " Goddess暗戀目標打飯完成.");
}
}
public static void main(String[] args) throws Exception {
Thread beiTaiThread = Thread.currentThread();
GoddessLikefriend goddessLikefriend = new GoddessLikefriend();
Thread gbf = new Thread(goddessLikefriend);
Goddess goddess = new Goddess(gbf);
//Goddess goddess = new Goddess();
Thread g = new Thread(goddess);
g.start();
gbf.start();
System.out.println("備胎開始排隊打飯.....");
g.join();
SleepTools.second(2);//讓主線程休眠2秒
System.out.println(Thread.currentThread().getName() + " 備胎打飯完成.");
}
}
SleepTools工具類代碼如下
public class SleepTools {
/**
* 按秒休眠
* @param seconds 秒數(shù)
*/
public static final void second(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
}
}
/**
* 按毫秒數(shù)休眠
* @param seconds 毫秒數(shù)
*/
public static final void ms(int seconds) {
try {
TimeUnit.MILLISECONDS.sleep(seconds);
} catch (InterruptedException e) {
}
}
}
輸出結(jié)果:
Goddess開始排隊打飯.....
備胎開始排隊打飯.....
Goddess暗戀目標開始排隊打飯.....
Thread-0 Goddess暗戀目標打飯完成.
Thread-1 Goddess打飯完成.
main 備胎打飯完成.
3.setPriority(int)
在 Java 線程中,通過一個整型成員變量 priority 來控制優(yōu)先級,優(yōu)先級的范 圍從 1~10,在線程構(gòu)建的時候可以通過 setPriority(int)方法來修改優(yōu)先級,默認 優(yōu)先級是 5,優(yōu)先級高的線程分配時間片的數(shù)量要多于優(yōu)先級低的線程。 設置線程優(yōu)先級時,針對頻繁阻塞(休眠或者 I/O 操作)的線程需要設置較 高優(yōu)先級,而偏重計算(需要較多 CPU 時間或者偏運算)的線程則設置較低的 優(yōu)先級,確保處理器不會被獨占。在不同的 JVM 以及操作系統(tǒng)上,線程規(guī)劃會 存在差異,有些操作系統(tǒng)甚至會忽略對線程優(yōu)先級的設定。
4.setDaemon(true)
Daemon(守護)線程是一種支持型線程,因為它主要被用作程序中后臺調(diào) 度以及支持性工作。這意味著,當一個 Java 虛擬機中不存在非 Daemon 線程的 時候,Java 虛擬機將會退出??梢酝ㄟ^調(diào)用 Thread.setDaemon(true)將線程設置 為 Daemon 線程。我們一般用不上,比如垃圾回收線程就是 Daemon 線程。
Daemon 線程被用作完成支持性工作,但是在 Java 虛擬機退出時 Daemon 線 程中的 finally 塊并不一定會執(zhí)行。在構(gòu)建 Daemon 線程時,不能依靠 finally 塊中 的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯。
public class DaemonThread {
private static class UseThread extends Thread{
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + isInterrupted());
} finally {
//守護線程中finally不一定起作用
System.out.println(" .............finally");
}
}
}
public static void main(String[] args)
throws InterruptedException{
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
// useThread.interrupt();
}
}
當用戶線程結(jié)束時,守護線程也同時結(jié)束,finally不一定會執(zhí)行,用戶線程里的finally肯定會執(zhí)行,只有線程為守護線程的時候,finally可能不會執(zhí)行。
5.sleep()
面試中經(jīng)常會被問到sleep方法對鎖的影響,用代碼驗證一下
public class SleepLock {
private Object lock = new Object();
public static void main(String[] args) {
SleepLock sleepTest = new SleepLock();
Thread threadA = sleepTest.new ThreadSleep();
threadA.setName("ThreadSleep");
Thread threadB = sleepTest.new ThreadNotSleep();
threadB.setName("ThreadNotSleep");
threadA.start();
try {
Thread.sleep(1000);
System.out.println(" Main slept!");
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
private class ThreadSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" will take the lock");
try {
synchronized(lock) {
System.out.println(threadName+" taking the lock");
Thread.sleep(5000);
System.out.println("Finish the work: "+threadName);
}
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
private class ThreadNotSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" will take the lock time="+System.currentTimeMillis());
synchronized(lock) {
System.out.println(threadName+" taking the lock time="+System.currentTimeMillis());
System.out.println("Finish the work: "+threadName);
}
}
}
}
ThreadSleep will take the lock
ThreadSleep taking the lock
Main slept!
ThreadNotSleep will take the lock time=1606999948908
Finish the work: ThreadSleep
ThreadNotSleep taking the lock time=1606999952908
Finish the work: ThreadNotSleep
從輸出結(jié)果看,調(diào)用了sleep方法以后,鎖并沒有釋放
線程間的共享和協(xié)作
1.synchronized
Java 支持多個線程同時訪問一個對象或者對象的成員變量,關(guān)鍵字 synchronized 可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線 程在同一個時刻,只能有一個線程處于方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱為內(nèi)置鎖機制。
對象鎖和類鎖: 對象鎖是用于對象實例方法,或者一個對象實例上的,類鎖是用于類的靜態(tài) 方法或者一個類的 class 對象上的。我們知道,類的對象實例可以有很多個,但 是每個類只有一個 class 對象,所以不同對象實例的對象鎖是互不干擾的,但是 每個類只有一個類鎖。
不加鎖,可能造成的現(xiàn)象,例如
public class SynTest {
private long count =0;
private Object obj = new Object();//作為一個鎖
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*用在同步塊上*/
public void incCount(){
// synchronized (obj){
count++;
// }
}
/*用在方法上*/
public synchronized void incCount2(){
count++;
}
/*用在同步塊上,但是鎖的是當前類的對象實例*/
public void incCount3(){
synchronized (this){
count++;
}
}
//線程
private static class Count extends Thread{
private SynTest simplOper;
public Count(SynTest simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest simplOper = new SynTest();
//啟動兩個線程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000
}
}
啟動兩個線程,分別對simplOper對象中count值進行10000次count++操作,理論上我們最后輸出的數(shù)值應該為20000,但是實際運行結(jié)果中,只會偶爾輸出20000的結(jié)果。
加鎖以后,我們就會得到我們想要的值了
public class SynTest {
private long count =0;
private Object obj = new Object();//作為一個鎖
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*用在同步塊上*/
public void incCount(){
synchronized (obj){
count++;
}
}
/*用在方法上*/
public synchronized void incCount2(){
count++;
}
/*用在同步塊上,但是鎖的是當前類的對象實例*/
public void incCount3(){
synchronized (this){
count++;
}
}
//線程
private static class Count extends Thread{
private SynTest simplOper;
public Count(SynTest simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest simplOper = new SynTest();
//啟動兩個線程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000
}
}
當然我們也可以使用incCount2方法或者incCount3方法,incCount2為同步方法,incCount3為對象鎖,都會達到加鎖的效果
錯誤的加鎖方式,例如
public class TestIntegerSyn {
public static void main(String[] args) throws InterruptedException {
Worker worker=new Worker(1);
//Thread.sleep(50);
for(int i=0;i<5;i++) {
new Thread(worker).start();
}
}
private static class Worker implements Runnable{
private Integer i;
private Object o = new Object();
public Worker(Integer i) {
this.i=i;
}
@Override
public void run() {
synchronized (i) {
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"--@"
+System.identityHashCode(i));
i++;
System.out.println(thread.getName()+"-------"+i+"-@"
+System.identityHashCode(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName()+"-------"+i+"--@"
+System.identityHashCode(i));
}
}
}
}
輸出結(jié)果
Thread-0--@1096443612
Thread-0-------2-@1335726215
Thread-4--@1335726215
Thread-4-------3-@357858746
Thread-0-------3--@357858746
Thread-4-------3--@357858746
Thread-3--@357858746
Thread-3-------4-@1892057330
Thread-3-------4--@1892057330
Thread-2--@1892057330
Thread-2-------5-@1488708714
Thread-2-------5--@1488708714
Thread-1--@1488708714
Thread-1-------6-@390233048
Thread-1-------6--@390233048
從輸出結(jié)果看,Thread-3--@357858746的位置輸出的不對,可見我們加的鎖并沒有起到作用,原因是在執(zhí)行i++的過程中,i的對象已經(jīng)改變,我們可以反編譯一下class文件可以看到,每次執(zhí)行i++都是調(diào)用的Integer的valueOf方法,而valueOf方法是new 一個新的對象,鎖的對象發(fā)生了改變,這是一個錯誤的加鎖方式。
2.volatile 最輕量的同步機制
volatile 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的,但是volatile不能保證操作的原子性。
volatile 最適用的場景:一個線程寫,多個線程讀
