Java多線程
程序、進程和線程
程序(program)
概念:是爲完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的代碼。
進程(process)
概念:程序的一次執行過程,或是正在運行的一個程序。
說明:進程作爲資源分配的單位,系統在運行時會爲每個進程分配不同的內存區域
線程(thread)
概念:進程可進一步細化爲線程,是一個程序內部的一條執行路徑。
說明:線程作爲調度和執行的單位,每個線程擁獨立的運行棧和程序計數器(pc),線程切換的開銷小。
JVM內存結構圖
進程可以細化爲多個線程。
每個線程,擁有自己獨立的:棧、程序計數器
多個線程,共享同一個進程中的結構:方法區、堆。
並行與併發
-
單核CPU與多核CPU的理解:
單核CPU,其實是一種假的多線程,因爲在一個時間單元內,也只能執行一個線程的任務。例如:雖然有多車道,但是收費站只有一個工作人員在收費,只有收了費才能通過,那麼CPU就好比收費人員。如果某個人不想交錢,那麼收費人員可以把他“掛起”(晾着他,等他想通了,準備好了錢,再去收費),但是因爲CPU時間單元特別短,因此感覺不出來。
如果是多核的話,才能更好的發揮多線程的效率。(現在的服務器都是多核的)
一個Java應用程序java.exe,其實至少三個線程:main()主線程,gc()垃圾回收線程,異常處理線程。當然如果發生異常,會影響主線程。 -
並行與併發的理解:
- 並行:多個CPU同時執行多個任務。比如:多個人同時做不同的事。
- 併發:一個CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事
Thread類的構造器
- Thread():創建新的Thread對象
- Thread(String threadName):創建線程並指定線程實例名
- Thread(Runnable target):指定創建線程的目標對象,它實現了Runnable接口中的run方法
- Thread(Runnable target, String name):創建新的Thread對象
繼承Thread類方式創建多線程
-
創建一個繼承於Thread類的子類(extends Thread)
-
子類重寫Thread類的run()方法 --> 將此線程執行的操作寫在run()方法體中
-
創建Thread類的子類對象(創建線程對象)
-
通過此對象調用start():①啓動當前線程 ② 調用當前線程的run()
- 說明:
- 問題一:我們啓動一個線程,必須調用start(),不能調用run()(相當於main線程調用普通方法)的方式啓動線程。
- 問題二:如果再啓動一個線程,必須重新創建一個Thread子類的對象,調用此對象的start()。
//1. 創建一個繼承於Thread類的子類
class MyThread extends Thread {
//2. 重寫Thread類的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 創建Thread類的子類的對象
MyThread t1 = new MyThread();
//4.通過此對象調用start():①啓動當前線程 ② 調用當前線程的run()
t1.start();
//問題一:我們不能通過直接調用run()的方式啓動線程。
// t1.run();
//問題二:再啓動一個線程,遍歷100以內的偶數。不可以還讓已經start()的線程去執行。會報IllegalThreadStateException
// t1.start();
//我們需要重新創建一個線程的對象
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然是在main線程中執行的。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
}
}
}
}
實現Runnable接口方式創建多線程
-
創建一個實現了Runnable接口的類(implement Runnable)
-
實現類去實現Runnable中的抽象方法:run()
-
創建實現類的對象
-
將此對象作爲參數傳遞到Thread類的構造器中,創建Thread類的對象
-
通過Thread類的對象調用start()
//1. 創建一個實現了Runnable接口的類
class MThread implements Runnable{
//2. 實現類去實現Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 創建實現類的對象
MThread mThread = new MThread();
//4. 將此對象作爲參數傳遞到Thread類的構造器中,創建Thread類的對象
Thread t1 = new Thread(mThread);
t1.setName("線程1");
//5. 通過Thread類的對象調用start():① 啓動線程 ②調用當前線程的run()-->調用了Runnable類型的target的run()
t1.start();
//再啓動一個線程,遍歷100以內的偶數
Thread t2 = new Thread(mThread);
t2.setName("線程2");
t2.start();
}
}
兩種方式的對比
-
開發中:優先選擇:實現Runnable接口的方式
原因:
-
實現的方式沒有類的單繼承侷限性
-
實現的方式更適合來處理多個線程共享數據的情況(類的屬性共享相當於添加static修飾了屬性)
-
-
聯繫:public class Thread implements Runnable(Thread類本身也實現了Runnable接口 )
-
相同點:
兩種方式都需要重寫run(),將線程要執行的邏輯聲明在run()中。
兩種方式啓動線程,都是調用的Thread類中的start()。
Thread類中的常用方法
-
void start():啓動當前線程;調用當前線程的**run()**方法
-
run(): 通常需要重寫Thread類中的此方法,將創建的線程要執行的操作聲明在此方法中
-
static Thread currentThread():返回當前線程的靜態方法
-
String getName():獲取當前線程的名字
-
void setName(String name):設置當前線程的名字
-
static void yield():線程讓步,釋放當前cpu的執行權(若隊列中沒有同優先級的線程,忽略此方法)
-
join():在線程a中調用線程b的join(),此時線程a就進入阻塞狀態,直到線程b完全執行完以後,線程a才結束阻塞狀態
-
stop():已過時。當執行此方法時,強制結束當前線程
-
static void sleep(long millis):讓當前線程“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間內,當前線程是阻塞狀態
-
boolean isAlive():判斷當前線程是否存活
線程的優先級
-
線程的優先級等級:
-
MAX_PRIORITY:10
-
MIN_PRIORITY:1
-
NORM_PRIORITY:5 (爲默認優先級)
-
-
如何獲取和設置當前線程的優先級:
-
getPriority():獲取線程的優先級
-
setPriority(int newPriority):設置/改變線程的優先級
說明:
- 線程創建時繼承父線程的優先級
- 高優先級的線程要搶佔低優先級線程cpu的執行權。但只是從概率上講,高優先級的線程高概率的情況下被執行。並不意味着只當高優先級的線程執行完以後,低優先級的線程才執行。
-
-
線程通信:wait() 、notify() 、notifyAll() :此三個方法定義在Object類中的
- 線程的分類:一種是守護線程,一種是用戶線程
- 守護線程是用來服務用戶線程的,通過在start()方法前調用**thread.setDaemon(true)**可以把一個用戶線程變成一個守護線程。
- Java垃圾回收就是一個典型的守護線程。
- 若JVM中都是守護線程,當前JVM將退出。
Thread的生命週期
線程同步機制
- 通過同步機制,來解決線程的安全問題
-
方式一:同步代碼塊
-
操作共享數據的代碼,即爲需要被同步的代碼。 -->不能包含代碼多了,也不能包含代碼少了。
-
共享數據:多個線程共同操作的變量。比如:ticket就是共享數據。
-
同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。要求:多個線程必須要共用同一把鎖。
-
補充:在實現Runnable接口創建多線程的方式中,我們可以考慮使用this充當同步監視器。在繼承Thread類創建多線程的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器。
-
-
方式二:同步方法
如果操作共享數據的代碼完整的聲明在一個方法中,我們可以將此方法聲明同步的。
-
關於同步方法的總結:
-
同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。
-
非靜態的同步方法,同步監視器是:this
-
靜態的同步方法,同步監視器是:當前類本身
-
-
-
方式三:Lock鎖 — JDK5.0新增
class Window implements Runnable{ private int ticket = 100; //1.實例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while(true){ try{ //2.調用鎖定方法lock() lock.lock(); if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":售票,票號爲:" + ticket); ticket--; }else{ break; } }finally { //3.調用解鎖方法:unlock() lock.unlock(); } } } }
死鎖的理解
-
不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
-
說明:
- 出現死鎖後,不會出現異常,不會出現提示,只是所的線程都處於阻塞狀態,無法繼續
- 我們使用同步時,要避免出現死鎖。
-
解決方法
- 使用專門的算法、原則
- 儘量減少同步資源的定義
- 儘量避免嵌套同步
線程通信三個方法
- wait():一旦執行此方法,當前線程就進入阻塞狀態,並釋放同步監視器。
- notify():一旦執行此方法,就會喚醒被wait()的一個線程。如果有多個線程被wait,就喚醒優先級高的那個。
- notifyAll():一旦執行此方法,就會喚醒所有被wait的線程。
- 說明:
- wait(),notify(),notifyAll()三個方法必須使用在同步代碼塊或同步方法中。
- wait(),notify(),notifyAll()三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器。否則,會出現IllegalMonitorStateException異常。(同步監視器應相同!!)
- wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中。
釋放鎖和不釋放鎖的情況
-
釋放鎖
- 當前線程的同步方法、同步代碼塊執行結束。
- 當前線程在同步代碼塊、同步方法中遇到break、return終止了該代碼塊、該方法的繼續執行。
- 當前線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束。
- 當前線程在同步代碼塊、同步方法中執行了線程對象的**wait()**方法,當前線程暫停,並釋放鎖。
-
不釋放鎖線
- 程執行同步代碼塊或同步方法時,程序調用**Thread.sleep()、Thread.yield()**方法暫停當前線程的執行
- 線程執行同步代碼塊時,其他線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放鎖(同步監視器)。應儘量避免使用suspend()和resume()來控制線程(方法已過時)
生產者消費者問題
-
生產者(Productor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。
-
分析:
-
是否是多線程問題?是,生產者線程,消費者線程
-
是否有共享數據?是,店員(或產品)
-
如何解決線程的安全問題?同步機制,有三種方法
-
是否涉及線程的通信?是
class Clerk{
private int productCount = 0;
//生產產品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":開始生產第" + productCount + "個產品");
//喚醒
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消費產品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":開始消費第" + productCount + "個產品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生產者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":開始生產產品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消費者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":開始消費產品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生產者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消費者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消費者2");
p1.start();
c1.start();
c2.start();
}
}
實現Callable接口創建多線程
如何理解實現Callable接口的方式創建多線程比實現Runnable接口創建多線程方式強大?
-
call()可以有返回值的。
-
call()可以拋出異常,被外面的操作捕獲,獲取異常的信息
-
Callable是支持泛型
//1.創建一個實現Callable的實現類
class NumThread implements Callable{
//2.實現call方法,將此線程需要執行的操作聲明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.創建Callable接口實現類的對象
NumThread numThread = new NumThread();
//4.將此Callable接口實現類的對象作爲傳遞到FutureTask構造器中,創建FutureTask的對象
FutureTask futureTask = new FutureTask(numThread);
//5.將FutureTask的對象作爲參數傳遞到Thread類的構造器中,創建Thread對象,並調用start()
new Thread(futureTask).start();
try {
//6.獲取Callable中call方法的返回值
//get()返回值即爲FutureTask構造器參數Callable實現類重寫的call()的返回值。
Object sum = futureTask.get();
System.out.println("總和爲:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
使用線程池創建多線程
好處:
-
提高響應速度(減少了創建新線程的時間)
-
降低資源消耗(重複利用線程池中線程,不需要每次都創建)
-
便於線程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大線程數
- keepAliveTime:線程沒有任務時最多保持多長時間後會終止
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定線程數量的線程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//設置線程池的屬性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.執行指定的線程的操作。需要提供實現Runnable接口或Callable接口實現類的對象
service.execute(new NumberThread());//適合適用於Runnable
service.execute(new NumberThread1());//適合適用於Runnable
// service.submit(Callable callable);//適合使用於Callable
//3.關閉連接池
service.shutdown();
}
}
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
面試題
談談你對程序、進程、線程的理解
- 程序:指一段靜態的代碼
- 進程:正在運行的一個程序
- 線程:一個程序內部的一條執行路徑
寫一個線程安全的單例模式。
餓漢式、懶漢式:
//懶漢式
class Bank{
private Bank(){
}
private static Bank instance = null;
public static Bank getInstance(){
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
synchronized 與 Lock的異同?
-
相同點:二者都可以解決線程安全問題
-
不同:
- synchronized機制在執行完相應的同步代碼以後,自動的釋放同步監視器
- Lock需要手動的啓動同步(lock()),同時結束同步也需要手動的實現(unlock())
優先使用順序:
Lock --> 同步代碼塊(已經進入了方法體,分配了相應資源) --> 同步方法(在方法體之外)
sleep() 和 wait()的異同?
-
相同點:一旦執行方法,都可以使得當前的線程進入阻塞狀態。
-
不同點:
- 兩個方法聲明的位置不同:Thread類中聲明sleep() , Object類中聲明wait()
- 調用的要求不同:sleep()可以在任何需要的場景下調用。 wait()必須使用在同步代碼塊或同步方法中
- 關於是否釋放同步監視器:如果兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。
兩個線程交替打印1-100?
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Java中多線程的創建有幾種方式?(四種)
用什麼關鍵字修飾同步方法?
stop()和suspend()方法爲何不推薦使用?
- 繼承Thread類方式創建多線程(1.5前)
- 實現Runnable接口方式創建多線程(1.5前)
- 實現Callable接口創建多線程
- 使用線程池創建多線程
用synchronized關鍵字修飾同步方法
反對使用stop(),是因爲它不安全。它會解除由線程獲取的所有鎖定,而且如果對象處於一種不連貫狀態,那麼其他線程能在那種狀態下檢查和修改它們。結果很難檢查出真正的問題所在。suspend()方法容易發生死鎖。調用suspend()的時候,目標線程會停下來,但卻仍然持有在這之前獲得的鎖定。此時,其他任何線程都不能訪問鎖定的資源,除非被"掛起"的線程恢復運行。對任何線程來說,如果它們想恢復目標線程,同時又試圖使用任何一個鎖定的資源,就會造成死鎖。所以不應該使用suspend(),而應在自己的Thread類中置入一個標誌,指出線程應該活動還是掛起。若標誌指出線程應該掛起,便用wait()命其進入等待狀態。若標誌指出線程應當恢復,則用一個notify()重新啓動線程。
sleep() 和 wait() 有什麼區別?
- sleep是線程類(Thread)的方法,導致此線程暫停執行指定時間,給執行機會給其他線程,但是監控狀態依然保持,到時後會自動恢復。調用sleep不會釋放對象鎖。
- wait是Object類的方法,對此對象調用wait方法導致本線程放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象發出notify方法(或notifyAll)後本線程才進入對象鎖定池準備獲得對象鎖進入運行狀態。
同步和異步有何異同,在什麼情況下分別使用他們?舉例說明。
答:如果數據將在線程間共享。例如正在寫的數據以後可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那麼這些數據就是共享數據,必須進行同步存取。
當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。
啓動一個線程是用run()還是start()?
答:啓動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM調度並執行。這並不意味着線程就會立即運行。run()方法就是正常的對象調用方法的執行,並不是使用分線程來執行的。
當一個線程進入一個對象的一個synchronized方法後,其它線程是否可進入此對象的其它方法?
答:不能,一個對象的一個synchronized方法只能由一個線程訪問。
請說出你所知道的線程同步的方法。
答:wait():使一個線程處於等待狀態,並且釋放所持有的對象的lock。
sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException異常。
notify():喚醒一個處於等待狀態的線程,注意的是在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且不是按優先級。
notityAll():喚醒所有處入等待狀態的線程,注意並不是給所有喚醒線程一個對象的鎖,而是讓它們競爭。
多線程有幾種實現方法,都是什麼?同步有幾種實現方法,都是什麼?
答:多線程有兩種實現方法,分別是繼承Thread類與實現Runnable接口
同步的實現方面有兩種,分別是synchronized,wait與notify
線程的基本概念、線程的基本狀態以及狀態之間的關係
答:線程指在程序執行過程中,能夠執行程序代碼的一個執行單位,每個程序至少都有一個線程,也就是程序本身。
Java中的線程有四種狀態分別是:創建、就緒、運行、阻塞、結束
簡述synchronized和java.util.concurrent.locks.Lock的異同 ?
答:主要相同點:Lock能完成synchronized所實現的所有功能
主要不同點:Lock有比synchronized更精確的線程語義和更好的性能。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,並且必須在finally從句中釋放。
案例:三個線程間的通訊
public class Demo01 {
public static void main(String[] args) {
//三個線程間的通訊
MyTask task = new MyTask();
new Thread(){
public void run() {
while(true){
try {
task.task1();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
new Thread(){
public void run() {
while(true){
try {
task.task2();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
new Thread(){
public void run() {
while(true){
try {
task.task3();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
class MyTask{
//標識 1:可以執行任務1,2:可以執行任務2, 3:可以執行任務3
int flag = 1;
public synchronized void task1() throws InterruptedException{
if(flag != 1){
this.wait();//當前線程等待
//this.wait(timeout);
}
System.out.println("1.銀行信用卡自動還款任務...");
flag = 2;
//this.notify();//喚醒隨機線程
this.notifyAll();//喚醒所有等待線程
}
public synchronized void task2() throws InterruptedException{
if(flag != 2){
this.wait();//線程等待
}
System.out.println("2.銀行儲蓄卡自動結算利息任務...");
flag = 3;
//this.notify();//喚醒其它線程
this.notifyAll();
}
public synchronized void task3() throws InterruptedException{
if(flag != 3){
this.wait();//線程等待
}
System.out.println("3.銀行短信提醒任務...");
flag = 1;
//this.notify();//喚醒其它線程
this.notifyAll();
}
}
class MyTask{
//標識 1:可以執行任務1,2:可以執行任務2, 3:可以執行任務3
int flag = 1;
public synchronized void task1() throws InterruptedException{
if(flag != 1){
this.wait();//當前線程等待
//this.wait(timeout);
}
System.out.println("1.銀行信用卡自動還款任務...");
flag = 2;
//this.notify();//喚醒隨機線程
this.notifyAll();//喚醒所有等待線程
}
public synchronized void task2() throws InterruptedException{
if(flag != 2){
this.wait();//線程等待
}
System.out.println("2.銀行儲蓄卡自動結算利息任務...");
flag = 3;
//this.notify();//喚醒其它線程
this.notifyAll();
}
public synchronized void task3() throws InterruptedException{
if(flag != 3){
this.wait();//線程等待
}
System.out.println("3.銀行短信提醒任務...");
flag = 1;
//this.notify();//喚醒其它線程
this.notifyAll();
}
}