說在前面,我自己是不喜歡看特別長篇的博客。我能理解看長篇博客的那種難受,因此,想直接入正題看編碼的不妨跳過前兩塊內容,直接進入“線程的創建”。
線程概述
什麼是線程?
之前看到過某大神這樣一句話:比較糟糕的技術文檔的特徵:用專業名詞來解釋專業名詞
從這句話中,我受益匪淺,因此以後的文章儘量都避免這樣。相信大家也不喜歡看一些概念性的東西吧。
介紹線程之前,首先得解釋一下進程,因爲要用進程來解釋線程。
進程:是程序的一次動態執行過程。比如打開谷歌瀏覽器,從它打開的時刻開始就啓動了一個進程。
多進程:多進程就像打開了多個程序,比如邊玩QQ,邊聽歌,還可以瀏覽網頁,多個任務同時運行。
線程:比進程更小的執行單位,通常一個進程擁有1-n個線程。
多線程:指在同一程序(進程)中能夠同時處理多個任務,而這些任務就對應多個線程,比如瀏覽器下載圖片能夠同時下載多張圖片。比如ABC三個用戶同時訪問淘寶網,淘寶網的服務器收到A用戶的請求後,爲A用戶創建了一個線程,BC用戶同樣擁有自己對應的線程。注意:多線程不是爲了提高程序執行速度(性能甚至更低),而是提高應用程序的使用效率。
線程的生命週期
每一個生命週期也是一種狀態。
1、新建:創建一個新的線程對象
2、就緒:線程對象創建後,其他線程調用了該對象的start()方法,該線程被放入可運行的線程池中,等待獲取CPU的使用權。
(這裏可以這樣解釋,百米賽跑上,每個運動員對應一個線程,裁判員大喝“預備”這聲預備相當於調用了start()方法,所有運動員進入就緒狀態,開槍這個動作則對應了獲取CPU使用權,運動員得到命令開跑)注意:一個CPU核心在某一時刻只能運行一個線程,這裏的CPU相當於多核CPU,不扯遠了。。。
3、運行:就緒狀態的線程獲取了CPU,執行。(運動員開跑)
4、阻塞:阻塞情況比較複雜,這裏簡單考慮一下就不分類了,即線程因爲某種原因放棄CPU使用權,線程暫停。(某運動員搶跑,此次比賽暫停,所有運動員回起跑線重新準備就緒,注意不能繼續跑!也就是說不能到運行狀態)
5、死亡:線程執行完成或異常退出,該線程生命週期結束。
狀態轉換圖如下:
JAVA線程的創建
好了,上面囉嗦了這麼多,不過相信大家已經對線程有了一定理解吧,回到也許是大家最想看到的部分,coding...
1、繼承Thread類
這裏寫一個簡單的類,主線程輸出1-5,兩個子線程輸出1-10
public class FirstThread extends Thread {
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + " " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
new FirstThread().start();
new FirstThread().start();
}
}
運行截圖(部分):
可以看出,線程的執行是無序的,線程的執行是根據CPU分配情況來決定的。
2、實現Runnable接口
package cn.thread;
public class SecondThread implements Runnable {
private int money=500;//卡上還剩500元
@Override
public void run() {
while(true) {
String name = Thread.currentThread().getName();
if (name.equals("Son")) {
if (money <= 0) {
System.out.println("爸,我沒錢了!");
return;//Son線程的run方法結束
}
money = money - 300;//兒子一次取300元
System.out.println(name + "卡上還剩:" + money + "元");
}
if (name.equals("Father")) {
if (money >= 500) {
System.out.println("卡上錢還夠用!");
return;//Father線程的run方法結束
}
money = money + 1000;//父親一次存1000
System.out.println(name + "卡上還剩:" + money + "元");
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) {
SecondThread st = new SecondThread();
new Thread(st, "Son").start();
new Thread(st, "Father").start();
}
}
運行截圖:
繼承Thread類和實現Runnable接口該用哪一個?
結論網上一大堆,都可以搜到:繼承Thread類不適合資源共享,實現Runnable接口更具有靈活性。
我這裏就來解釋一下這個結論,深入理解一下它實現的原理
分析一下這兩種方法創建線程的原理,看Thread類的源碼,如圖:
Thread類實現了Runnable接口並重寫了run()方法,如果target(目標)爲空,則不執行代碼。
所以我們這裏繼承Thread類重寫run()方法的原因就在此。代碼如下:
public class FirstThread extends Thread {
public void run() { //該線程要執行的操作
}
public static void main(String args[]) {
new FirstThread().start();
}
}
繼續看,當target不爲空時,運行target的run()方法,而變量target看源碼
是一個Runnable對象,也就是說只要target這個對象不爲空,則調用Runnable對象的run()方法,再把Runnable對象傳遞給Thread類。代碼如下:
public class SecondThread implements Runnable {
@Override
public void run() {
}
public static void main(String args[]) {
SecondThread st = new SecondThread();
new Thread(st, "線程1").start();
new Thread(st, "線程2").start();
}
}
注意看這兩行代碼:
new Thread(st, "線程1").start();
new Thread(st, "線程2").start();
傳給Thread的是同一個Runnable對象,這也就是爲什麼說實現Runnable接口,可以資源共享,因爲操作的都是同一個對象啊。而繼承Thread每次都要new一個新的對象,對象自然就不同了。並且,實現Runnable接口這種方式更體現了面向對象這種思維,new一個線程,線程裏面傳一個對象,這個對象封裝了一系列操作。
JAVA線程同步的與通信
主要是以實際例子,應用來說明。
這裏先解釋一下互斥和同步(之後的緩存池也要用到)
互斥:是指散佈在不同進程之間的若干程序片斷,當某個進程運行其中一個程序片段時,其它進程就不能運行它們之中的任一程序片段,只能等到該進程運行完這個程序片段後纔可以運行。
同步:上一段代碼沒的完成,下一段必須等到上一段代碼完成後纔可以執行(必須嚴格按照規定的 某種先後次序來運行)。如買票排隊
(同步是一種更爲複雜的互斥,而互斥是一種特殊的同步。)
異步:上一段代碼沒的完成,下一段不必等到上一段代碼完成就可以執行。如手機發送短信。
1、synchronized同步
線程安全問題這裏舉個例子用火車購票來解釋,庫存100張票,三個窗口同時售票。
方法一:直接給方法加上synchronized關鍵字修飾
package cn.thread;
public class SynchronizedSaleTickets implements Runnable {
private static int tickets = 100;
@Override
public void run() {
while (tickets>0) {//餘票充足
try {
Thread.sleep(100);//延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
if(tickets==0){
System.out.println("票已售完!");
}
}
synchronized static void sale() {
tickets--;
if(tickets>0) {
System.out.println(Thread.currentThread().getName() + "當前餘票:" + tickets);
}
}
public static void main(String[] args){
SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
new Thread(sst).start();
new Thread(sst).start();
new Thread(sst).start();
new Thread(sst).start();
}
}
運行結果(部分):
方法二:定義一個共同的“鎖”
代碼和上面大致相同,修改了一點:
package cn.thread;
public class SynchronizedSaleTickets implements Runnable {
private static int tickets = 100;
/*
鎖住一個“對象”(門栓),這個對象可以是一個引用對象,也可以是此對象內部的某個數據
這裏定義了一個對象
*/
private static Object lock=new Object();
@Override
public void run() {
while (tickets>0) {//餘票充足
try {
Thread.sleep(100);//延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
if(tickets==0){
System.out.println("票已售完!");
}
}
static void sale() {
synchronized (lock) {//“鎖住”要執行的操作
tickets--;
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "當前餘票:" + tickets);
}
}
}
public static void main(String[] args){
SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
new Thread(sst).start();
new Thread(sst).start();
new Thread(sst).start();
new Thread(sst).start();
}
}
注意:鎖是上在要操作的資源內部方法中,而不是上在線程代碼中!
2、wait、notify通信
這裏舉一個例子,也是一道java線程面試題
子線程循環10次,接着主線程循環100次,又接着回到子線程循環10次,再接着回到主線程又循環100次,如此循環50次。
代碼如下:
package cn.thread;
public class ThreadCommunication {
public static void main(String[] args) {
final Business business = new Business();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
business.sub(i);
}
}
}).start();
for (int i = 1; i <= 50; i++) {
business.main(i);
}
}
//互斥代碼放在資源內部方法中,而不是放在線程代碼中!
static class Business {
private boolean shouldSub = true;
//子線程業務邏輯
public synchronized void sub(int i) {
//這裏用while比用if好,用if表示如果被通知喚醒,則繼續執行後續的代碼,而用while表示被通知喚醒過後
//再回來檢查參數是否該執行,邏輯更嚴密
while (!shouldSub) {//不該子線程執行
try {
this.wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
System.out.println("sub thread:" + j + ",loop of " + i);
}
//子線程執行完通知主線程
shouldSub = false;
this.notify();
}
//主線程業務邏輯
public synchronized void main(int i) {
while (shouldSub) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
System.out.println("main thread:" + j + ",loop of" + i);
}
//主線程執行完通知子線程
shouldSub = true;
this.notify();
}
}
}
運行結果(部分):
3、線程間共享數據
這個例子和火車售票不一樣,四個線程,兩個線程對每次J+2,兩個線程每次對J-1。
注意:共享的數據封裝到了一個單獨的類中
package cn.thread;
public class MultiThreadShareData {
public static void main(String[] args) {
final ShareData1 data2 = new ShareData1();//共享數據data2
//Thread-0和Thread-1每次-1
new Thread(new MyRunnable1(data2)).start();
new Thread(new MyRunnable1(data2)).start();
////Thread-2和Thread-3每次+2
new Thread(new MyRunnable2(data2)).start();
new Thread(new MyRunnable2(data2)).start();
}
}
class MyRunnable1 implements Runnable {//減法
private ShareData1 data1;
public MyRunnable1(ShareData1 data1) {
this.data1 = data1;
}
public void run() {
while (true) {
data1.decrement();
System.out.println(Thread.currentThread().getName() + "線程每次-1,當前:" + data1.getJ());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyRunnable2 implements Runnable {//加法
private ShareData1 data1;
public MyRunnable2(ShareData1 data1) {
this.data1 = data1;
}
public void run() {
while (true) {
data1.increment();
System.out.println(Thread.currentThread().getName() + "線程每次+2,當前:" + data1.getJ());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ShareData1 {//共享數據存放在此類的對象中,還有基本的操作
private int j = 0;
public int getJ() {
return j;
}
public void setJ(int j) {
this.j = j;
}
public synchronized void increment() {
j = j + 2;
}
public synchronized void decrement() {
j--;
}
}
下篇繼續講解:
線程池(緩存池)
線程相關類
線程面試經典題目
......
原文出自:https://my.csdn.net/qq_37094660(如需轉載請註明出處)