多線程
JAVA多線程是Java面試中比較重要的常考點,而且使用起來也需要有一定的技術功底,這裏就是主要講下多線程的基礎知識。
一、基本概念:程序、進程、線程、併發、並行
注1:一個Java應用程序java.exe,其實至少有三個線程:main()主線程,gc()垃圾回收線程,異常處理線程。當然如果發生異常,會影響主線程。
注2:Java中的線程分爲兩類:一種是守護線程,一種是用戶線程。它們在幾乎每個方面都是相同的,唯一的區別是判斷JVM何時離開。守護線程是用來服務用戶線程的,通過在start()方法前調用thread.setDaemon(true)可以把一個用戶線程變成一個守護線程。Java垃圾回收就是一個典型的守護線程。若JVM中都是守護線程,當前JVM將退出。
二、線程的創建和使用 java.lang.Thread
① 繼承Thread類
-
- 創建一個繼承於Thread類的子類
-
- 重寫Thread類的run() --> 將此線程執行的操作聲明在run()中
-
- 創建Thread類的子類的對象
-
- 通過此對象調用start()
注意點:
② 實現Runnable接口
- 通過此對象調用start()
-
- 創建一個實現了Runnable接口的類
-
- 實現類去實現Runnable中的抽象方法:run()
-
- 創建實現類的對象 ( MThread mThread = new MThread();)
-
- 將此對象作爲參數傳遞到Thread類的構造器中,創建Thread類的對象
(Thread t1 = new Thread(mThread);)
- 將此對象作爲參數傳遞到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();
}
}
注:繼承方式和實現方式的聯繫與區別
public class Thread extends Object implements Runnable{ }
開發中:優先選擇:實現Runnable接口的方式
原因: 1. 實現的方式沒有類的單繼承性的侷限性。
2. 實現的方式更適合來處理多個線程有共享數據的情況。
聯繫 : 兩種方式都需要重寫run(),將線程要執行的邏輯聲明在run()中。
③ JDK5.0新增實現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();
}
}
}
如何理解實現Callable接口的方式創建多線程比實現Runnable接口創建多線程方式強大?
* 1. call()可以有返回值的。
-
- call()可以拋出異常,被外面的操作捕獲,獲取異常的信息
-
- Callable是支持泛型的
④ JDK5.0新增使用線程池
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);
}
}
}
}
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();
}
}
三、 java.lang.Thread 類
構造器
Thread(): 創建新的Thread對象
Thread(String threadname): 創建線程並指定線程實例名
Thread(Runnable target):指定創建線程的目標對象,實現Runnable接口中的run方法
Thread(Runnable target, String name): 創建新的Thread對象
方法
-
- start(): 啓動當前線程;調用當前線程的run()
-
- run(): 通常需要重寫Thread類中的此方法,將創建的線程要執行的操作聲明在此方法中
-
- currentThread(): 靜態方法,返回執行當前代碼的線程
-
- getName(): 獲取當前線程的名字
-
- setName(): 設置當前線程的名字
-
- yield(): 釋放當前cpu的執行權,線程讓步,暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程
-
- join(): 在線程a中調用線程b的join(),此時線程a就進入阻塞狀態,直到線程b完全執行完以後,線程a才結束阻塞狀態,低優先級的線程也可以獲得執行。
-
- stop(): 已過時。當執行此方法時,強制結束當前線程。
-
- sleep(long millitime): 讓當前線程“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間內,當前線程是阻塞狀態,拋出InterruptedException異常。
-
- isAlive(): 判斷當前線程是否存活。
-
- getPriority(): 獲取線程的優先級 (優先級 :1 – 10 )
-
- setPriority(int p): 設置線程的優先級
注:高優先級的線程要搶佔低優先級線程cpu的執行權。但是隻是從概率上講,高優先級的線程高概率的情況下被執行。並不意味着只有當高優先級的線程執行完以後,低優先級的線程才執行。
四、線程的生命週期
JDK中用Thread.State類定義了線程的幾種狀態
要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命週期中通常要經歷如下的五種狀態:
① 新建 :當Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態
② 就緒 :處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源
③ 運行 :當就緒的線程被調度並獲得CPU資源時,便進入運行狀態, run()方法定義了線程的操作和功能
④ 阻塞 :在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態
⑤ 死亡 :線程完成了它的全部工作或線程被提前強制性地中止或出現異常導致結束
- setPriority(int p): 設置線程的優先級
五、線程的同步
① Synchronized 同步代碼塊
格式:synchronized (同步監視器){
// 需要被同步的代碼;
}
說明:
- 操作共享數據的代碼,即爲需要被同步的代碼。–>不能包含代碼多了,也不能包含代碼少了。
- 共享數據:多個線程共同操作的變量。比如:ticket就是共享數據。
- 同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。
要求:多個線程必須要共用同一把鎖。 - 補充:在實現Runnable接口創建多線程的方式中,我們可以考慮使用this充當同步監視器。
② Synchronized 方法
格式: public synchronized void show (String name){
….
}
注:
1、操作同步代碼時,只能有一個線程參與,其他線程等待。相當於是一個單線程的過程,效率低。 — 侷限性
2、同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。
3、 非靜態的同步方法,同步監視器是:this
靜態的同步方法,同步監視器是:當前類本身
③ Lock鎖 — JDK5.0新增
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保證線程安全的代碼;
}finally{
//注意:如果同步代碼有異常,要將unlock()寫入finally語句塊
lock.unlock();
}
}
}
注:synchronized 與 Lock 的對比
六、線程通信
面試題:sleep() 和 wait()的異同?
1.相同點:一旦執行方法,都可以使得當前的線程進入阻塞狀態。
2.不同點:
1)兩個方法聲明的位置不同:Thread類中聲明sleep() , Object類中聲明wait()
2)調用的要求不同:sleep()可以在任何需要的場景下調用。 wait()必須使用在同步代碼塊或同步方法中
3)關於是否釋放同步監視器:如果兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。
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();
}
}
}
}
}
補充:生產者 / 消費者經典問題
生產者(Productor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,
店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員
會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。
這裏可能出現兩個問題:
生產者比消費者快時,消費者會漏掉一些數據沒有取到。
消費者比生產者快時,消費者會取相同的數據。
class Clerk { // 售貨員
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println("生產者生產了第" + product + "個產品");
notifyAll();
}
}
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消費者取走了第" + product + "個產品");
product--;
notifyAll();
}
}
}
class Productor implements Runnable { // 生產者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生產者開始生產產品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable { // 消費者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消費者開始取走產品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}
public class ProductTest { //測試
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
}
}