文章目錄
每日一考和複習
每日一考
- 談談你對程序、進程、線程的理解
// 我的答案
1.程序:
2.進程:資源分配的最小單位,一個進程可以包含多個線程
3.線程:獨立執行的最小單位,同進程中的多個線程共享同樣的資源(堆,方法區)
// 參考答案
1.程序是爲完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的代碼,靜態對象
2.進程是程序的一次執行過程,或是 正在運行的一個程序。是一個動態的過程:有它自身的產生、存在和消亡的過程
3.線程:進程可進一步細化爲線程,是一個程序內部的一條執行路徑,線程作爲調度和執行的單位,每個線程擁有獨立的運行棧和程序計數器
- 代碼完成繼承Thread的方式創建分線程,並遍歷100以內的自然數
package com.water.java;
/**
* @auther water
* @create 2020-04-07-18:22
*/
public class MyThread extends Thread{
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.start();
}
}
- 代碼完成實現Runnable接口的方法創建分線程,並遍歷100以內的自然數
package com.water.java;
/**
* @auther water
* @create 2020-04-07-18:27
*/
public class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new MyThread2());
thread1.start();
}
}
- 對比兩種創建方式
//我的答案
1.實現接口可以多繼承,繼承Thread類只能單繼承
2.實現接口的一個對象可以被對個線程調用,且資源共享
//參考答案
相同點:都需要重寫run(),將線程要執行的邏輯聲明在run()中
不同點:
1.實現的方式沒有類的單繼承性的侷限性
2.實現的方式更適合來處理多個線程有共享數據的情況
- 說說你對IDEA中Project和Module的理解
//我的答案
(記混了,寫的模板的理解)一段代碼的縮寫,經常使用的代碼塊或語句可以使用模塊,使用更加方便
//參考答案
project相當於Eclipse中的workspace,module相當於真正的project
複習
day18的學習內容
多線程(接day18)
線程的生命週期
- JDK中用Thread.State類定義了線程的幾種狀態
要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命週期中通常要經歷如下的五種狀態:
- 新建: 當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態
- 就緒:處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源
- 運行:當就緒的線程被調度並獲得CPU資源時,便進入運行狀態, run()方法定義了線程的操作和功能
- 阻塞:在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態
- 死亡:線程完成了它的全部工作或線程被提前強制性地中止或出現異常導致結束
線程的同步
- 多線程出現了安全問題
- 問題的原因:
當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。 - 解決辦法:
對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行,使用同步代碼塊解決繼承Thread類的方式的線程安全問題
- Synchronized的使用方法
- Java對於多線程的安全問題提供了專業的解決方式:同步機制
// 使用方法
synchronized(同步監視器){
//需要被同步的代碼
}
- 說明:
1.操作共享數據的代碼,即爲需要被同步的代碼。 -->不能包含代碼多了,也不能包含代碼少了
2.共享數據:多個線程共同操作的變量
3.同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖
要求:多個線程必須要共用同一把鎖
while (true) {
synchronized (this) {
if (ticket > 0) {// 也可用-類名.class,反射暫時瞭解
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":賣票,票號爲:" + ticket);
ticket--;
} else {
break;
}
}
}
說明:在繼承Thread類創建多線程的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器
- synchronized還可以放在方法聲明中,表示整個方法爲同步方法
private synchronized void show() {//同步監視器:this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);
ticket--;
}
}
關於同步方法的總結:
- 同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。
- 非靜態的同步方法,同步監視器是:this 靜態的同步方法,同步監視器是:當前類本身
優點: 同步的方式,解決了線程的安全問題
侷限性: 操作同步代碼時,只能有一個線程參與,其他線程等待。相當於是一個單線程的過程,效率低
餓漢式線程安全問題解決
class Bank {
private Bank() {
}
private static Bank instance = null;
//方式一:效率稍差
public static Bank getInstance() {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
//方式二:效率更高
public static Bank getInstance2() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
死鎖
-
死鎖的理解:不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
-
說明:
①出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續
②我們使用同步時,要避免出現死鎖
//死鎖的示例
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
- 解決方法
①專門的算法、原則
②儘量減少同步資源的定義
③儘量避免嵌套同步
Lock
- 從JDK 5.0開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用Lock對象充當
- java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象
- ReentrantLock 類實現了 Lock ,它擁有與synchronized 相同的併發性和內存語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖
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();
}
}
}
}
小結
-
面試題:synchronized 與 Lock的異同?
相同:二者都可以解決線程安全問題
不同:synchronized機制在執行完相應的同步代碼以後,自動的釋放同步監視器
Lock需要手動的啓動同步(lock()),同時結束同步也需要手動的實現(unlock()) -
優先使用順序:
Lock -> 同步代碼塊(已經進入了方法體,分配了相應資源) -> 同步方法(在方法體之外)
線程的通信
-
涉及到的三個方法:
wait():一旦執行此方法,當前線程就進入阻塞狀態,並釋放同步監視器
notify():一旦執行此方法,就會喚醒被wait的一個線程。如果有多個線程被wait,就喚醒優先級高的那個
notifyAll():一旦執行此方法,就會喚醒所有被wait的線程 -
說明:
- wait(),notify(),notifyAll()三個方法必須使用在同步代碼塊或同步方法中
- wait(),notify(),notifyAll()三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器
否則,會出現IllegalMonitorStateException異常 - wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中
//線程通信的例子:使用兩個線程打印 1-100。線程1, 線程2 交替打印
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得調用如下wait()方法的線程進入阻塞狀態
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("線程1");
t2.setName("線程2");
t1.start();
t2.start();
}
}
☆面試題:sleep() 和 wait()的異同?
- 相同點:一旦執行方法,都可以使得當前的線程進入阻塞狀態
- 不同點:
①兩個方法聲明的位置不同:Thread類中聲明sleep() , Object類中聲明wait()
②調用的要求不同:sleep()可以在任何需要的場景下調用。 wait()必須使用在同步代碼塊或同步方法中
③關於是否釋放同步監視器:如果兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖
生產者/消費者問題
-
問題描述:
生產者(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();
}
}
JDK5.0新增線程創建方式
新增方式一:實現Callable接口
- 與使用Runnable相比, Callable功能更強大些
- 相比run()方法,可以有返回值
- 方法可以拋出異常
- 支持泛型的返回值
- 需要藉助FutureTask類,比如獲取返回結果
//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接口創建多線程方式強大?
- call()可以有返回值的。
- call()可以拋出異常,被外面的操作捕獲,獲取異常的信息
- Callable是支持泛型的
新增方式二:使用線程池
- 背景: 經常創建和銷燬、使用量特別大的資源,比如併發情況下的線程,對性能影響很大
- 思路: 提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷燬、實現重複利用。類似生活中的公共交通工具
- 好處:
① 提高響應速度(減少了創建新線程的時間)
② 降低資源消耗(重複利用線程池中線程,不需要每次都創建)
③ 便於線程管理
corePoolSize:核心池的大小
maximumPoolSize:最大線程數
keepAliveTime:線程沒有任務時最多保持多長時間後會終止
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();
}
}
面試題:創建多線程有幾種方式?四種!