Java多執行緒面試點
- 執行緒和行程
- 執行緒和行程關系和差別,優缺點
- 為什么執行緒的程式計數器是私有的
- 執行緒共享的堆和方法區作用
- 并發和并行的區別
- 多執行緒的必要性
- 多執行緒可能存在的問題
- 執行緒的各種狀態
- 什么是背景關系切換
- 死鎖的產生和避免
- sleep()和wait()方法區別和共同點
- 執行緒啟動為什么用start()而不用run()
- 對sychrnozed關鍵字的看法
- sychrnozed關鍵字三大關鍵用法
- 雙重校驗鎖實作單利模式
- synchronized的JVM原理實作
- synchronized和ReentrantLock
- 關鍵字Volatile,防止指令重排,原理
- sychronized和volatile的區別
- ThreadLocal
- ThreadLocal的原理和泄露問題
- 執行緒池
- 多執行緒的Atomic原子類
執行緒和行程
行程
行程就是程式執行的一次程序,一般代碼檔案存在磁盤,用.exe檔案結束,系統運行一個程式就是行程的創建,運行和消亡的程序,在java中我們執行main方法其實就是啟動了一個jvm行程,而main所在的就是一個執行緒,稱為主執行緒,
執行緒
執行緒稱為輕量級的行程,一個行程可以包含多個執行緒,一個類的多執行緒共享堆和方法區的資源,但是卻擁有自己獨立的程式計數器,虛擬機堆疊,本地方法堆疊,
查看java有哪些執行緒運行
public static void main(String[] args) throws IOException {
//獲取java執行緒管理MxBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//獲取執行緒和執行緒堆疊資訊
ThreadInfo[] infos = threadMXBean.dumpAllThreads(false,false);
//遍歷
for(ThreadInfo info : infos){
System.out.println("["+info.getThreadId()+"]"+info.getThreadName());
}
// [6]Monitor Ctrl-Break
// [5]Attach Listener 添加事件的執行緒
// [4]Signal Dispatcher 分發處理給JVM的信號執行緒
// [3]Finalizer 呼叫物件finalizer方法的執行緒
// [2]Reference Handler 去除Reference執行緒
// [1]main 主執行緒
}
執行緒和行程關系和差別,優缺點
關系 執行緒是行程的更小單位,一個行程可以擁有多個執行緒
差別 執行緒共享堆和方法區,但是每個執行緒有著自己獨立的jvm堆疊,本地方法堆疊和程式計數器,
優缺點行程是相互獨立的,相對安全,方便資源的管理和保護,而執行緒因為共享堆和方法區,所以相對上是不利于資源的管理和保護的,

為什么執行緒的程式計數器是私有的
程式計數器作用
- 節碼解釋器通過改變程式計數器來讀取指令,從而實作代碼流程的控制
- 當執行緒阻塞等情況時,程式計數器會記錄執行緒執行到了哪里,當重新開始時會從記錄點開始執行,
//私有化的主要原因:記錄執行緒執行到的位置
虛擬機堆疊和本地方法堆疊
虛擬機堆疊創建的堆疊幀用來存盤區域變數,運算元堆疊,常量池等資訊,一個方法的執行到結束就表示一個出堆疊和入堆疊的程序,
本地方法站和虛擬機堆疊基本功能一致,差別是本地方法堆疊為虛擬機堆疊的Native方法服務,
堆疊私有化原因:
為了保證執行緒的區域變數不被訪問到,
執行緒共享的堆和方法區作用
堆是行程里面最大的一塊記憶體,主要用來存放新建的物件,方法區用來存放已經被加載的類的資訊,常量,靜態變數和即時編譯器編譯后的代碼等資料,
并發和并行的區別
并發:同一時間段內,多個任務都在執行,(其實就是多執行緒類似,任務都在執行卻不代表同一時間任務都在執行,而是同一時間段)
并行:同一時間,多個任務同時進行,
多執行緒的必要性
- 多執行緒是輕量級的行程,執行緒之間的調換和切換成本遠小于行程,多CPU計算機下,多執行緒可以減少執行緒背景關系切換開銷
- 多執行緒是高并發的基礎,
- 多執行緒可以提高CPU的利用率
- 在多核CPU下,單執行緒只能呼叫一個CPU,但是多執行緒可以呼叫多個CPU同時運行,
多執行緒可能存在的問題
執行緒死鎖,記憶體泄漏,背景關系切換,受限于硬體和軟體的資源閑置問題,
執行緒的各種狀態

什么是背景關系切換
總結:背景關系切換就是一個執行緒(任務)從被保存到在加載的一個程序,
因為一個執行緒只能呼叫一個CPU,但是多執行緒的執行緒個數有時遠遠大于CPU個數,所以CPU會分給執行緒一個時間片,當一個執行緒的時間片到的時候執行緒就會加入就緒狀態,直到下次繼續運行,
死鎖的產生和避免
產生:
當兩個執行緒同時想擁有對方的資源,但又鎖住了自己的要被呼叫的資源,這樣兩個執行緒就會一直在就緒狀態,(一個執行緒在就緒狀態下還鎖住這其他執行緒需要的資源)

避免:
- 改變代碼執行順序,破壞回圈等待條件
- 當一個執行緒進一步申請不到資源時主動釋放占有的資源
- 一次性呼叫所有的資源
sleep()和wait()方法區別和共同點
區別
- 最大區別:sleep方法不會讓執行緒釋放占有的資源,而wait可以讓執行緒釋放資源,
- sleep方法在休眠事件過去后會自動蘇醒,而wait方法不會,它需要其他執行緒使用notify或者notifyAll方法喚醒,
- sleep通常用于執行緒的休眠,而wait用于執行緒通信
共同點 - 兩者都可以讓執行緒暫停執行
執行緒啟動為什么用start()而不用run()
執行緒在start方法呼叫后處于就緒狀態,執行緒啟動時要先獲得CPU的調度,獲得調度后才可以運行起來,
如果直接呼叫執行緒的run方法,就相當于在main執行緒里面呼叫這個執行緒的run方法一樣,
對sychrnozed關鍵字的看法
這個關鍵字用來給執行緒資源加鎖,讓被鎖住的資源同時只能被一個執行緒呼叫,
在老版本的jdk中它屬于重量級鎖,但在之后的版本中對其進行了優化,用了很多重鎖來減少鎖的開銷,
sychrnozed關鍵字三大關鍵用法
修飾方法
相當于對當前物件加鎖,進入方法時需要獲得當前物件
修飾靜態方法
相當于對當前類的鎖,而不是物件,所以當一個執行緒獲取當前類的非靜態synchrnized方法,而另外一個執行緒獲取當前類的靜態synchronized方法不會出現執行緒堵塞,因為一個鎖物件,一個鎖類,
修飾代碼塊
指定物件加鎖,加入代碼塊時需要先鎖定指定的物件,
注意:不要這樣寫:sychronized(String s);因為String類在常量池又快取功能,
雙重校驗鎖實作單利模式
必要性:
當多個執行緒同時呼叫單例獲取方法時,如果單例使用的是懶漢式,就可能出現有執行緒獲得的單例為空的現象,這時就要用到雙重校驗鎖了,
public class Test2 {
//必須要寫private和volatile
//volatile主要是防止指令重拍,導致test2不為空,但是又沒有初始化,
private volatile static Test2 test2;
private Test2(){
}
public Test2 getInstance(){
//使用雙重檢驗
if(test2==null){
synchronized (Test2.class){
if(test2==null){
test2 = new Test2();
}
}
}
return test2;
}
}
synchronized的JVM原理實作
修飾代碼塊
使用的是monitorenter和monitorexit指令,一個指向同步代碼塊開始,一個指向結束為止,開始monitorenter指令試圖獲取鎖,獲取到后計數器從0變為1,當指向monitorexit指令,表示代碼塊同步結束,計數器變為0.
修飾方法
不是指令,而是使用一個標識,jvm通過表示判斷這個是不是同步方法,從而實作同步呼叫,
synchronized和ReentrantLock
- 都是可重入鎖,自己內部也可以在獲取外部鎖住的物件,
- 前者依賴JVM,后者依賴API
- 后者比前者多一些高級功能,
關鍵字Volatile,防止指令重排,原理
java記憶體模型:
- 記憶體出現問題原因:老版本時是直接從記憶體讀取資料,但現在的版本執行緒會把變數儲存在本地記憶體中,也就是暫存器,所以當主存的資料改變時,執行緒可能任然從本地記憶體讀取資料,這是就會造成結果有問題,
- 解決方法:將變數定義為volatile型別,告訴系統這是一個不穩定的變數,是要放在主記憶體的,其實就是防止指令重排,
并發編程的三個重要點
- 原子性:一個或者多個操作時,所有操作不能受到任何因素的影響,要么都執行,要么就都不執行,sychronized關鍵字就可以做到原子性,
- 可見性:當一個變數在變化時,其他執行緒也可以看見,volatile就可以實作,
- 有序性:就是volatile防止指令重排,
sychronized和volatile的區別
- volatile也可以實作執行緒的同步,但是它是輕量級的同步,所以效率要高于sychronized,
- volatile只能修飾屬性,但是sychronized可以修飾方法和代碼塊,
- volatile不會發生阻塞,sychronized可能會發生阻塞,
- volatile保證代碼的可見性,但是不保證原子性,但是synchronized都可以保證,
ThreadLocal
1.ThreadLocal簡介:
讓每個執行緒擁有自己獨立的屬性,
public class Test2 implements Runnable{
//這就是執行緒獨立屬性的寫法
private static final ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
@Override
public void run() {
try {
//通過threadLocal.get()方式得到獨立的屬性
//通過set()方式設定獨立的屬性
//這里輸出沒改變之前的值
System.out.println("ThreadName = " + Thread.currentThread().getName() +
"default format = " + threadLocal.get().toPattern());
Thread.sleep(100);
//這里測驗改變后會不會影響其他執行緒的
threadLocal.set(new SimpleDateFormat());
System.out.println("ThreadName = " + Thread.currentThread().getName() +
"changed format = " + threadLocal.get().toPattern());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Test2 test2 = new Test2();
for(int i = 0;i < 10;i++){
Thread t = new Thread(test2,""+i);
Thread.sleep(100);
t.start();
}
}
}
//輸出結果(可見這個執行緒改變值沒有影響其他執行緒的值)
ThreadName = 0 default format = yyyyMMdd HHmm
ThreadName = 1 default format = yyyyMMdd HHmm
ThreadName = 0 changed format = yy-M-d ah:mm
ThreadName = 1 changed format = yy-M-d ah:mm
ThreadName = 2 default format = yyyyMMdd HHmm
ThreadName = 2 changed format = yy-M-d ah:mm
ThreadName = 3 default format = yyyyMMdd HHmm
ThreadName = 3 changed format = yy-M-d ah:mm
ThreadName = 4 default format = yyyyMMdd HHmm
ThreadName = 4 changed format = yy-M-d ah:mm
ThreadName = 5 default format = yyyyMMdd HHmm
ThreadName = 5 changed format = yy-M-d ah:mm
ThreadName = 6 default format = yyyyMMdd HHmm
ThreadName = 6 changed format = yy-M-d ah:mm
ThreadName = 7 default format = yyyyMMdd HHmm
ThreadName = 7 changed format = yy-M-d ah:mm
ThreadName = 8 default format = yyyyMMdd HHmm
ThreadName = 8 changed format = yy-M-d ah:mm
ThreadName = 9 default format = yyyyMMdd HHmm
ThreadName = 9 changed format = yy-M-d ah:mm
ThreadLocal的原理和泄露問題
原理
將執行緒要獨立的物件存盤在ThreaLocalMap里面
泄露問題
ThreadLocalMap的key是弱參考,而value是強參考,所以ThreadLocal如果沒有被強參考,垃圾回收可能會將key去除,導致value泄露問題,
執行緒池
為什么要用執行緒池
- 降低資源消耗
- 提高回應速度
- 提高執行緒的管理性能
Runnable和Callable的區別
- 前者不會回傳結果或者拋出例外,后者可以,
執行緒池的創建
//不允許使用Executors去創建執行緒池,要用ThreadPoolExecutors去創建,
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//執行緒池類
public class Test2{
//執行緒池的一些常數的定義
private static final int CORE_POOL_SIZE = 5;//最小可以同時運行的執行緒數量
private static final int MAX_POOL_SIZE = 10;//最大執行緒樹
private static final int QUEUE_CAPACITY = 100;//阻塞佇列大小,當執行緒多時會被放在這個佇列
//當執行緒池的執行緒數量大于corepoolsize的時候,又沒有任務結束,這時會等待keepalivetime
//當過了這個時間,核心執行緒外的執行緒會被銷毀
private static final Long KEEP_ALIVE_TIME = 1L;
//開始創建執行緒池,并運行執行緒
public static void main(String[] args) {
//用阿里巴巴的方式創建執行緒池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
//執行緒池的使用
for(int i = 0;i < 10;i++){
Runnable thread = new TestThreadPool(""+i);
//執行緒加入執行緒池并執行
executor.execute(thread);
}
//執行緒池的終止
executor.shutdown();
while(!executor.isTerminated()){
}
System.out.println("Over!");
}
}
class TestThreadPool implements Runnable{
//用來記錄當前執行緒的名字
private String name;
public TestThreadPool(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("start:"+this.name+"->"+new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end:"+this.name+"->"+new Date());
}
}
//運行結果(由結果可以知道,每次只有5個執行緒同時執行)
start:3->Wed Sep 09 15:40:48 CST 2020
start:2->Wed Sep 09 15:40:48 CST 2020
start:4->Wed Sep 09 15:40:48 CST 2020
start:1->Wed Sep 09 15:40:48 CST 2020
start:0->Wed Sep 09 15:40:48 CST 2020
end:3->Wed Sep 09 15:40:53 CST 2020
start:5->Wed Sep 09 15:40:53 CST 2020
end:2->Wed Sep 09 15:40:53 CST 2020
start:6->Wed Sep 09 15:40:53 CST 2020
end:4->Wed Sep 09 15:40:53 CST 2020
start:7->Wed Sep 09 15:40:53 CST 2020
end:1->Wed Sep 09 15:40:53 CST 2020
start:8->Wed Sep 09 15:40:53 CST 2020
end:0->Wed Sep 09 15:40:53 CST 2020
start:9->Wed Sep 09 15:40:53 CST 2020
end:5->Wed Sep 09 15:40:58 CST 2020
end:6->Wed Sep 09 15:40:58 CST 2020
end:7->Wed Sep 09 15:40:58 CST 2020
end:9->Wed Sep 09 15:40:58 CST 2020
end:8->Wed Sep 09 15:40:58 CST 2020
Over!
執行緒池里面幾個重要的引數
上面的代碼里面有了
多執行緒的Atomic原子類
原子性:一個Atomic操作一旦執行是不可以中斷的,是不會被其它執行緒所干擾的,
作用:sychronized的消耗太高,而Atomic的原子類使用了CAS,volatile和native等實作了原子化,提高了執行效率,保證了執行緒的安全和同步,
JUC包中的原子類
-
基本原子資料型別:
AtomicInteger:整形原子類
AtomicLong:長整形原子類
AtomicBoolean:布爾原子類 -
原子參考型別
AtomicReference:參考型別原子類
AtomicStampReference:原子更新帶有版本號的原子類
AtomicMarkableReference:原子跟新帶有標志位的類 -
物件的屬性修改型別
AtomicIntegerFieldUpdater:原子更新整形欄位更新器
AtomicLongFieldUpdaer:原子更新長整形欄位的更新器
AtomicInteger的使用
- 常用方法:
public final int get();//獲取當前的值
public final int getAndSet(int newValue);//獲取并更改為另外一個值
public final int getAndIncrement();//獲取當前的值并自增
public final int getAndDecrement();//獲取當前的值并自減
public final int getAndAdd(int value);//獲取當前的值+value的值
boolean compareAndSet(int except,int value);//如果當前值等于except,則將值更改為value
public final final void lazySet(int newValue);//最總更改為newValue,但是有延遲,導致其他執行緒可能獲取原來的值
使用例子
package com.how2java.tmallweb_springboot.test;
import java.util.concurrent.atomic.AtomicInteger;
//執行緒池類
public class Test2{
private AtomicInteger integer = new AtomicInteger();
//加1
public void increment(){
integer.incrementAndGet();
}
//自減
public void decreament(){
integer.decrementAndGet();
}
//增加數值
public void add(int value){
integer.getAndAdd(value);
}
//重新賦值
public void set(int value){
integer.getAndSet(value);
}
//得到當前值
public int get(){
return integer.get();
}
public static void main(String[] args) {
Test2 test2 = new Test2();
System.out.println(test2.get());
test2.add(10);
System.out.println(test2.get());
test2.set(50);
System.out.println(test2.get());
test2.increment();
System.out.println(test2.get());
/*
* 0
10
50
51
* */
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/12995.html
標籤:其他
