文章目錄
- 一、創建線程的幾種方式
- 二、查看進程的方法
- 三、線程運行原理--棧楨Debug
- 四、線程運行原理圖解
- 五、線程上下文切換(Thread Context Switch)
- 六、常用方法
- 6.1 run和start
- 6.2 sleep和yield
- 6.3 線程優先級
- 6.4 sleep方法的一個應用
- 6.5 join方法
- 6.6 join同步應用
- 6.7 join限時同步
- 6.8 interrupt打斷阻塞
- 6.9 interrupt打斷正常運行的線程
- 6.10 線程設計模式之兩段終止模式
- 6.11 靜態的Thread.interrupted()
- 6.12 interrupt打斷park
- 6.13 過時的方法
- 6.14 守護線程
- 6.15 線程的五種狀態
- 6.16 六種狀態
- 6.17 六種狀態的演示
- 6.18 臨界區與競態條件
- 七、線程安全問題分析
一、創建線程的幾種方式
二、查看進程的方法
三、線程運行原理–棧楨Debug
棧與棧幀
Java Virtual Machine Stacks (Java 虛擬機棧)
我們都知道 JVM 中由堆、棧、方法區所組成,其中棧內存是給誰用的呢?其實就是線程,每個線程啓動後,虛擬機就會爲其分配一塊棧內存。
- 每個棧由多個棧幀(Frame)組成,對應着每次方法調用時所佔用的內存
- 每個線程只能有一個活動棧幀,對應着當前正在執行的那個方法
- 棧楨以線程爲單位,相互之間是獨立的
public class Test {
public static void main(String[] args) {
method1(10);
}
public static void method1(int x){
int y = x + 1;
Object o = method2();
System.out.println(o);
}
public static Object method2(){
Object o = new Object();
return o;
}
}
下圖,走到斷點時,產生了一個main棧楨,棧楨裏面有一個局部變量:
方法走到下圖標記的位置的時候,有兩個棧楨,method1棧楨是新加入的,也有局部變量:
走到下圖標記的位置時,添加了method2棧楨,有三個棧楨:
走到下圖標記的時候,只有兩個棧在楨了,因爲走完method2,method2棧楨釋放了,同時,也說明一個問題,棧是後進先出的:
debug到這裏已經將問題說明白了,就不再繼續了.
四、線程運行原理圖解
4.1 類加載
加載字節碼文件,將字節碼文件加載到方法區的內存中,這裏爲了好理解,就沒有寫二進制的代碼了,寫的是java代碼.
4.2 啓動main線程
類加載完成後,JVM會啓動main線程,並且分配一塊棧內存給它。接下來這個線程就交給了任務調度器去調度執行,如果搶到CPU了,main方法是方法的執行入口,會給main方法分配一個棧楨內存.
棧內存中有局部變量表、返回地址、鎖記錄、操作數棧。main棧楨的局部變量表是args,返回地址是程序的退出地址。
程序計數器:記錄下一次該執行什麼命令,例如,記錄了下一個執行的方法method1(10)
繼續執行:
現在methd2方法被執行完了,需要釋放掉內存:
然後method1執行結束釋放內存,main執行完成,釋放內存。
五、線程上下文切換(Thread Context Switch)
因爲以下一些原因導致 cpu 不再執行當前的線程,轉而執行另一個線程的代碼
- 線程的 cpu 時間片用完
- 垃圾回收
- 有更高優先級的線程需要運行
- 線程自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法
當 Context Switch 發生時,需要由操作系統保存當前線程的狀態,並恢復另一個線程的狀態,Java 中對應的概念就是程序計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是線程私有的 - 狀態包括程序計數器、虛擬機棧中每個棧幀的信息,如局部變量、操作數棧、返回地址等
- Context Switch 頻繁發生會影響性能
六、常用方法
6.1 run和start
- 直接調用 run 是在主線程中執行了 run,沒有啓動新的線程
- 使用 start 是啓動新的線程,通過新的線程間接執行 run 中的代碼
6.2 sleep和yield
sleep
- 調用 sleep 會讓當前線程從 Running 進入 Timed Waiting 狀態(阻塞)
- 其它線程可以使用 interrupt 方法打斷正在睡眠的線程,這時 sleep 方法會拋出 InterruptedException
- 睡眠結束後的線程未必會立刻得到執行
- 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性
yield
- 調用 yield 會讓當前線程從 Running 進入 Runnable 就緒狀態,然後調度執行其它線程
- 具體的實現依賴於操作系統的任務調度器
對比:
1.就緒狀態,還是有機會被任務調度器調用的,但是阻塞狀態,任務調度器是不會分配時間片給這種狀態的線程的
2.sleep是有具體的等待時間可設置的,而yield幾乎是沒有等待時間。
sleep打斷:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
System.out.println("interrupt...");
t1.interrupt();
}
當然,推薦使用這樣的方式進行睡眠,代碼可讀性更好:
TimeUnit.SECONDS.sleep(2);
執行結果:
6.3 線程優先級
- 線程優先級會提示(hint)調度器優先調度該線程,但它僅僅是一個提示,調度器可以忽略它
- 如果 cpu 比較忙,那麼優先級高的線程會獲得更多的時間片,但 cpu 閒時,優先級幾乎沒作用
6.4 sleep方法的一個應用
while(true) {
try {
//Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面代碼在單核CPU下運行,CPU會佔用到100%,如果將註釋的代碼放開,即加上sleep方法,CPU只有3%左右。找一臺單核的linux虛擬機,使用top命令查看。
6.5 join方法
join方法:等待線程結束,誰來調用這個方法,就等待誰的線程結束。
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
System.out.println("開始");
Thread t1 = new Thread(() -> {
System.out.println("開始");
TimeUnit.SECONDS.sleep(1);
System.out.println("結束");
r = 10;
});
t1.start();
System.out.println("結果爲:" + r);
System.out.println("結束");
}
執行結果:
如果我們希望結果是10呢?
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
System.out.println("開始");
Thread t1 = new Thread(() -> {
System.out.println("開始");
TimeUnit.SECONDS.sleep(1);
System.out.println("結束");
r = 10;
});
t1.start();
t1.join();
System.out.println("結果爲:" + r);
System.out.println("結束");
}
上面代碼在start之後,添加了join方法,表示等t1線程結果返回,才能繼續往下執行。體現了同步應用。
6.6 join同步應用
加入兩個依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
在測試類上添加註解:
@Slf4j(topic = "c.Test")
static int r = 0;
static int r1 = 0;
static int r2 = 0;
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
執行結果:
如果將上面的兩個join方法調用位置,執行結果還是3ms。
6.7 join限時同步
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 線程執行結束會導致 join 結束
log.debug("join begin");
t1.join(1000);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
執行結果:
只在1003ms結束,所以r1的值還是0。
如果將t1.join(3000);
打印的結果爲:
r1的值已經是10了,耗時2000ms,說明join中的參數時間,如果大於線程的執行時間,就以線程執行完畢爲準,如果小於線程執行時間,就以設置的時間爲準,所以是限時同步。
6.8 interrupt打斷阻塞
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打斷標記:{}", t1.isInterrupted());
}
執行結果:
如果在sleep時被打斷,被標記爲true,但是sleep方法會清除標記,導致標記爲false。
視頻中說打斷標記爲false,但是這裏的結果是true,此處存疑!
解惑:
觀察上面結果,打斷標記的輸出,在異常拋出之前就輸出了
調試過程:
首先,我在catch塊中加入了System.out.println(Thread.currentThread().isInterrupted());
發現打印的結果是false,說明打斷標記確實爲false,再結合上面輸出結果,發現:打印語句其實在catch代碼塊執行之前執行了。所以,我們如果想要看到正確的結果,需要在打印語句之後休眠一段時間,完整代碼如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().isInterrupted());
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
Thread.sleep(1000);
log.debug("打斷標記:{}", t1.isInterrupted());
}
執行結果如下:
6.9 interrupt打斷正常運行的線程
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打斷了, 退出循環");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
執行結果:
使用這種方式可以優雅的終止一個線程,並不是立刻將線程殺死,而是給了線程一個料理後事的機會。
6.10 線程設計模式之兩段終止模式
在一個線程 T1 中如何“優雅”終止線程 T2?這裏的【優雅】指的是給 T2 一個料理後事的機會。
錯誤思路
使用線程對象的 stop() 方法停止線程
- stop 方法會真正殺死線程,如果這時線程鎖住了共享資源,那麼當它被殺死後就再也沒有機會釋放鎖,其它線程將永遠無法獲取鎖
- 使用 System.exit(int) 方法停止線程目的僅是停止一個線程,但這種做法會讓整個程序都停止
兩階段終止模式
@Slf4j(topic = "c.Test")
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.Test")
class TwoPhaseTermination {
// 監控線程
private Thread monitorThread;
// 停止標記
private volatile boolean stop = false;
// 判斷是否執行過 start 方法
private boolean starting = false;
// 啓動監控線程
public void start() {
synchronized (this) {
if (starting) { // false
return;
}
starting = true;
}
monitorThread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
// 是否被打斷
if (current.isInterrupted()) {
log.debug("料理後事");
break;
}
try {
Thread.sleep(1000);
log.debug("執行監控記錄");
} catch (InterruptedException e) {
e.printStackTrace();
current.interrupt();//再次調用,將false變爲true,執行下一次循環時,發現標記是true,執行料理後事的代碼
}
}
}, "monitor");
monitorThread.start();
}
// 停止監控線程
public void stop() {
stop = true;
monitorThread.interrupt();
System.out.println(Thread.currentThread().isInterrupted());
}
}
執行結果:
如果在sleep時被打斷,被標記爲true,但是sleep方法會清除標記,導致標記爲false,會拋出異常,進入catch代碼,執行catch代碼後,標記會記爲true。
如果在執行監控記錄時被打斷,不會拋出代碼,打斷標記被記爲true。
6.11 靜態的Thread.interrupted()
- Thread.interrupted();也是判斷線程是否被打斷,但是它會清除打斷標記
- isInterrupted方法判斷線程是否被打斷,但是它不會清除打斷標記
6.12 interrupt打斷park
打斷標記爲true的情況下,park會失效。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打斷狀態:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
}
執行結果:
上面的結果可見:輸出park後,由於調用了park方法,暫停了1s,後來執行了t1.interrupt();
打斷標記變爲true,導致了park失效,繼續執行後面的代碼。
當然,如果再是打斷標記爲false,park方法立即會生效。
例如將Thread.currentThread().isInterrupted()
變爲Thread.currentThread().interrupt()
6.13 過時的方法
- stop() 停止線程運行
- suspend() 掛起(暫停)線程運行
- resume() 恢復線程運行
6.14 守護線程
只要有一個線程運行,整個JAVA進程都不會結束
有一種特殊的線程叫做守護線程,只要其它非守護線程運行結束了,即使守護線程的代碼沒有執行完,也會強制結束。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("結束");
}, "t1");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("結束");
}
結果:
主線程結束了,即使t1線程沒有執行完,也會被結束。
- 垃圾回收器線程就是一種守護線程,如果程序停止了,垃圾回收線程也會被強制停止
- Tomcat 中的 Acceptor 和 Poller 線程都是守護線程,所以 Tomcat 接收到 shutdown 命令後,不會等待它們處理完當前請求
6.15 線程的五種狀態
這是從 操作系統 層面來描述的
- 【初始狀態】僅是在語言層面創建了線程對象,還未與操作系統線程關聯
- 【可運行狀態】(就緒狀態)指該線程已經被創建(與操作系統線程關聯),可以由 CPU 調度執行
- 【運行狀態】指獲取了 CPU 時間片運行中的狀態
當 CPU 時間片用完,會從【運行狀態】轉換至【可運行狀態】,會導致線程的上下文切換 - 【阻塞狀態】如果調用了阻塞 API,如 BIO 讀寫文件,這時該線程實際不會用到 CPU,會導致線程上下文切換,進入【阻塞狀態】等 BIO 操作完畢,會由操作系統喚醒阻塞的線程,轉換至【可運行狀態】。與【可運行狀態】的區別是,對【阻塞狀態】的線程來說只要它們一直不喚醒,調度器就一直不會考慮
調度它們 - 【終止狀態】表示線程已經執行完畢,生命週期已經結束,不會再轉換爲其它狀態
6.16 六種狀態
這是從 Java API 層面來描述的
根據 Thread.State 枚舉,分爲六種狀態
- NEW 線程剛被創建,但是還沒有調用 start() 方法,五種狀態的劃分是重疊的。
- RUNNABLE 當調用了 start() 方法之後,注意,Java API 層面的 RUNNABLE 狀態涵蓋了 操作系統 層面的
【可運行狀態】、【運行狀態】和【阻塞狀態】(由於 BIO 導致的線程阻塞,在 Java 裏無法區分,仍然認爲是可運行) - BLOCKED , WAITING , TIMED_WAITING 都是 Java API 層面對【阻塞狀態】的細分,後面會在狀態轉換一節詳述
- TERMINATED 當線程代碼運行結束
6.17 六種狀態的演示
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable 既有可能分到時間片,又可能沒有分到,都是runable狀態
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Test.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting 等待t2線程執行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Test.class) { // blocked 由於t4線程獲得了鎖,沒有釋放,導致t6一直獲取不到鎖
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
執行結果:
6.18 臨界區與競態條件
一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區
例如,下面代碼中的臨界區
static int counter = 0;
static void increment()
// 臨界區
{
counter++;
}
static void decrement()
// 臨界區
{
counter--;
}
競態條件 Race Condition
多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件。
七、線程安全問題分析
使用全局變量list:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
執行結果:
使用局部變量list:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
這個是線程安全的,沒有報錯。
下面這個例子同樣是使用局部變量,但是method方法是public的,被繼承重寫了:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
執行結果:
因爲子類重新開啓了一個線程,和之前的線程共享list,導致了線程安全問題,所以最好就是將method3方法變成私有的,不讓子類重寫。
常見的線程安全類:
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的類
案例分析:
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>(); //no
// 是否安全?
String S1 = "..."; //yes
// 是否安全?
final String S2 = "..."; //yes
// 是否安全?
Date D1 = new Date(); //no
// 是否安全?
final Date D2 = new Date(); //no
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述變量
}
}
servlet是運行在tomcat上的一個實例,是單實例的,被tomcat多個線程共享使用。
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl(); //no
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 記錄調用次數
private int count = 0; //no
public void update() {
// ...
count++;
}
}
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L; //no,MyAspect單例,多個線程可能共享這個變量
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
上例最好用環繞通知,做成局部變量。
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl(); //yes,不可變,沒有提供修改
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();//yes,雖然是成員變量,但是沒提供修改
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){ //yes
// ...
} catch (Exception e) {
// ...
}
}
}
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
其中 foo 的行爲是不確定的,可能導致不安全的發生,被稱之爲外星方法
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
上例泄露引用。