0、介紹
線程:多個任務同時進行,看似多任務同時進行,但實際上一個時間點上我們大腦還是只在做一件事情。程序也是如此,除非多核cpu,不然一個cpu里,在一個時間點里還是只在做一件事,不過速度很快的切換,造成同時進行的錯覺。
多線程:
方法間調用:普通方法調用,從哪里來到哪里去,是一條閉合的路徑;
使用多線程:開辟了多條路徑。
進程和線程:
也就是 Process 和 Thread ,本質來說,進程作為資源分配的單位,線程是調度和執行的單位。具體來說:
- 每個進程都有獨立的代碼和數據空間(進程上下文),進程間切換會有較大開銷,操作系統中同時運行多個任務就是進程;
- 線程可以看成輕量級的線程,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換的開銷較小,同一個應用程序里多個順序流在執行,他們就是線程,除了CPU外,不會為線程分配內存,它自己使用的是所屬進程的資源,線程組只能共享資源。
其他概念:
- 線程可以理解為一個獨立的執行路徑;
- 在程序運行的時候,即使沒有自己創建線程,后台也會存在gc線程、主線程等,而main() 就是主線程,是程序的入口點;
- 一個進程里如果開辟了多個線程,線程一旦開始運行,是由調度器安排的,和操作系統緊密相關,他們的安排人為沒法干預;
- 對於同一份資源操作,會涉及資源搶奪問題,需要加入並發控制;
- 線程會帶來cpu調度時間、並發控制等額外的開銷;
- 每個線程只在自己的工作內存交互,如果加載和存儲主內存控制不當,就會造成數據不一致,也就是線程不安全。
創建線程:
在 java 中,創建線程有 3 種方式:
- 繼承Thread類(重寫run方法);
- 實現Runnable接口(重寫run方法);
- 實現Callable接口(重寫call方法,這個是在j.u.c包下的)。
根據設計原則,不管是里氏替換原則,還是在工廠設計模式種,都提到過,盡量多用實現,少用繼承,所以一般情況下盡量使用第二種方法創建線程。
一、創建方法1:繼承Thread類
先直接看下面一個 demo
/*
創建方式1:繼承Thread + 重寫run
啟動方式:創建子類對象 + start
*/
public class StartThread extends Thread {
//線程入口點
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡覺ing ");
}
}
public static void main(String[] args) {
//創建子類對象
StartThread startThread = new StartThread();
//啟動,主意是start
startThread.start();
for (int i=0; i<50; i++){
System.out.print("吃飯ing ");
}
}
}
我們把上面的run方法成為線程的入口點,里面是線程執行的代碼,當程序運行之后,可以發現,每次的運行結果都是不一樣的。

可以看到這種隨機穿插執行的結果,這是由cpu去安排時間片,調度決定的。
到這里我們總結使用第一種方法創建線程的步驟就是:
- 創建子類對象,這個子類是繼承了Thread類的;
- 啟動,調用start方法,而不是run方法,start方法是把這個線程丟給cpu的調度器,讓他適時運行而不是立即運行。如果使用run方法,那么就是單純的執行,並沒有開啟多線程,會先執行完上面的內容,再往下走。
二、創建方法2:實現Runnable接口
這種方法是推薦的方式,和上一種寫法相比較,很簡單,只需要把 extends Thread 改成 implements Runnable ,其他的地方幾乎沒有變化。
區別在於,調用的時候,不能直接 start(),只能借助一個 Thread 對象作為代理。
/*
創建方式2:實現Runnable + 重寫run
啟動方式:創建實現類對象 + 借助thread代理類 + start
*/
public class StartThreadwithR implements Runnable {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡覺ing ");
}
}
public static void main(String[] args) {
StartThreadwithR startThread = new StartThreadwithR();
//創建代理類
Thread t = new Thread(startThread);
t.start();//啟動
for (int i=0; i<50; i++){
System.out.print("吃飯ing ");
}
}
}
總結第二種創建線程的方法步驟是:
- 創建實現類對象,實現類實現的是Runnable接口;
- 創建代理類Thread;
- 將實現類對象丟給代理類,然后用代理類start。
特殊的,如果我們的一個對象只使用一次,那就完全可以用匿名,上面的
StartThreadwithR startThread = new StartThreadwithR();
Thread t = new Thread(startThread);
t.start();
可以改成:
new Thread(new StartThreadwithR()).start();
兩種方法相比,因為推薦優先實現接口,而不是繼承類,所以第二種方法是推薦的。
三、可能出現的問題
3.1 黃牛訂票
當多個線程同時進行修改資源的時候,可能出現線程不安全的問題,最上面我們提到了,這里做一個簡單模擬。
假如三個黃牛同時在搶票,服務端的票數--的過程,對於三個線程可能會出現哪些問題呢?
/*
使用多線程修改資源帶來的線程安全問題
*/
public class Tickets implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while(true){
if (ticketNum<0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在搶票,余票" + ticketNum--);
}
}
//客戶端
public static void main(String[] args) {
Tickets tickets = new Tickets();
//多個Thread代理
new Thread(tickets,"黃牛1").start();
new Thread(tickets,"黃牛2").start();
new Thread(tickets,"黃牛3").start();
}
}
這里面用了簡單的模擬服務端和客戶端行為,請求票的時候,分別對票數進行 -- 操作,執行之后我們來看:

顯然出現了邏輯上的錯誤,因為多個線程的執行帶來的問題。
從運行結果的最后兩行入手,背后的原因是:
- 黃牛 2 先進入run;
- 可是到將票數-1之前,由於cpu的調度,黃牛 3 線程也開始執行,並且比黃牛 2 更快一步,直接進行了 -- 操作,票數變成了 0 ;
- 此時黃牛 2 輸出了結果,余票0;
- 隨后黃牛 3 線程才執行完輸出語句,票數反倒是 1 ?
如果我們再模擬一個網絡延遲,在 run 方法里加入:
//加入線程阻塞,模擬網絡延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
多運行幾遍,甚至可能票數變成負數。

顯然,如果在實際開發中,票數的變化,應該是嚴格遞減的過程,並且,余票到達 0 就應該 break,而不能還出現繼續執行了--操作,從而出現這種錯誤(不考慮退票之類的業務)。
這就是 高並發 問題,主要就是多線程帶來的安全問題。
3.2 龜兔賽跑
再來看一個例子,假如有烏龜和兔子進行賽跑,我們模擬兩個線程,分別對距離++。
/*
龜兔賽跑,借助Runnable和Thread代理
*/
public class Racer implements Runnable{
private String winner;
@Override
public void run() {
for (int dis=1; dis<=100; dis++){
System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
//每走一步,判斷是否比賽結束
if (gameOver(dis))break;
}
}
public boolean gameOver(int dis){
if (winner != null){
return true;
} else if (dis == 100){
winner = Thread.currentThread().getName();
System.out.println("獲勝者是 "+winner);
return true;
}
return false;
}
public static void main(String[] args) {
Racer racer = new Racer();//1.創建實現類
new Thread(racer,"兔子").start();//2.創建代理類並start
new Thread(racer,"烏龜").start();
}
}
這樣運行起來,總會有一個人贏,但是贏的每次不一定是哪一個。
四、創建方法3:實現Callable
面對高並發的情況,需要用到線程池。
來看重新實現的龜兔賽跑:
/*
創建方法3:Callable,是java.util.concurrent包里的內容
*/
public class RacerwithCal implements Callable<Integer> {
private String winner;
//需要實現的是call方法
@Override
public Integer call() throws Exception {
for (int dis=1; dis<=100; dis++){
System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
//每走一步,判斷是否比賽結束,並且結束可以有返回值
if (gameOver(dis))return dis;
}
return null;
}
public boolean gameOver(int dis){
if (winner != null){
return true;
} else if (dis == 100){
winner = Thread.currentThread().getName();
if (winner.equals("pool-1-thread-1"))System.out.println("獲勝者是 烏龜");
else System.out.println("獲勝者是 兔子");
return true;
}
return false;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.創建目標對象
RacerwithCal race = new RacerwithCal();
//2.創建執行服務,含有2個線程的線程池
ExecutorService service = Executors.newFixedThreadPool(2);
//3.提交執行
Future<Integer> result1 = service.submit(race);
Future<Integer> result2 = service.submit(race);
//4.獲取結果:pool-1-thread-1也就是第一個線程是烏龜,第二個兔子
Integer i = result1.get();
Integer j = result2.get();
System.out.println("比分是: "+ i + " : " + j);
//5.關閉服務
service.shutdownNow();
}
}
來看執行結果:

總結一下,步驟一般分為 5 步:
- 創建目標對象;
- 創建執行服務;
- 提交執行;
- 獲取結果;
- 關閉服務。
可以看到,這種方法的特殊之處在於:
- 目標類繼實現Callable接口的 call 方法,可以有返回值(前面的run是沒有返回值的);
- 不用處理異常,可以直接 throw;
- 使用的過程相比前兩種方法,變得復雜。
五、靜態代理模式
注意到在前面使用第二種方法創建多線程的時候,提到了 new Thread(tickets,"黃牛1").start(); 是使用了 Thread 作為代理。代理模式本身也是設計模式種的一種,分為動態代理和靜態代理,代理模式在開發中記錄日志等等很常用。
靜態代理的代理類是直接寫好的,拿過來用,動態代理則是在程序執行過程中臨時創建的。
在這里簡單介紹靜態代理。
實現一個婚慶公司,作為你的婚禮的代理,然后進行婚禮舉辦。
/*
靜態代理模式demo
1.真實角色
2.代理角色
3.1和2都實現同一個接口
*/
public class StaticProxy {
public static void main(String[] args) {
//完全類似於 new Thread(new XXX()).start();
new WeddingCompany(new You()).wedding();
}
}
//接口
interface Marry{
void wedding();
}
//真實角色
class You implements Marry{
@Override
public void wedding() {
System.out.println("結婚路上ing");
}
}
//代理角色
class WeddingCompany implements Marry{
//要代理的真實角色
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void wedding() {
ready();//准備
this.target.wedding();
after();//善后
}
private void after() {
System.out.println("結束ing");
}
private void ready() {
System.out.println("布置ing");
}
}
可以看到,最后的調用方法就相當於是寫線程的時候用到的 new Thread(new XXX()).start();
小小區別就在於,我們寫的線程類是實現的 run 方法,沒有實現start方法,但是不重要。
重要的是,代理類 可能做了很多的事,而中間需要 真實類 實現的一個方法必須實現,其他的方法,真實類不需要關心,也就是交給代理類去辦了。
六、Lambda表達式簡化線程
jdk1.8 后可以使用 lambda 表達式來簡化代碼,一般用在 只使用一次的、簡單的線程 里面。
簡化的寫法有很多,下面是逐漸簡化的過程。
6.1 靜態內部類
如果某個類只希望使用一次,可以用靜態內部類來實現,調用的時候一樣。
public class StartThreadLambda {
//靜態內部類
static class Inner implements Runnable{
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡覺ing ");
}
}
}
//靜態內部類
static class Inner2 implements Runnable{
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("吃飯ing ");
}
}
}
public static void main(String[] args) {
new Thread(new Inner()).start();
new Thread(new Inner2()).start();
}
}
使用靜態內部類的好處是,不使用的時候這個內部類是不會編譯的,這其實就是一個單例模式。
6.2 方法內部類
還可以直接寫到 main 方法內部,因為main 方法就是static,只啟動一次。
public class StartThreadLambda {
public static void main(String[] args) {
//方法內部類(局部內部類)
class Inner implements Runnable{
//。。。。。。
}
class Inner2 implements Runnable{
//。。。。。。
}
new Thread(new Inner()).start();
new Thread(new Inner2()).start();
}
}
6.3 匿名內部類
更進一步,可以直接利用匿名內部類,不用聲明出類的名稱來。
public class StartThreadLambda {
public static void main(String[] args) {
//匿名內部類,必須借助接口或者父類,因為沒有名字
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("吃飯ing ");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡覺ing ");
}
}
}).start();
}
}
這里面必須帶上實現體了就,因為沒有名字,那么就要借助父類或者接口,而父類或者接口的run方法是需要重寫/實現的。
6.4 Lambda表達式
jdk 8 對匿名內部類寫法再進行簡化,只用關注線程體,也就是只關注 run 方法里面的內容。
public class StartThreadLambda {
public static void main(String[] args) {
//使用Lambda表達式
new Thread(()-> {
for (int i=0; i<50; i++){
System.out.print("吃飯ing ");
}
}).start();
new Thread(()->{
for (int i=0; i<50; i++){
System.out.print("睡覺ing ");
}
}).start();
}
}
() - > 這個符號,編譯器就默認你是在實現 Runnable,並且默認是在實現 run 方法。
6.5 擴展
顯然,如果不是線程,是其他的我們自己寫的接口+實現類,Lambda表達式也是可用的,而且可以進行參數和返回值的擴展。
public class LambdaTest {
public static void main(String[] args) {
//直接使用lambda表達式實現接口
Origin o = (int a, int b)-> {
return a+b;
};
System.out.println(o.sum(100,100));
}
}
//自定義接口,相當於Runnable
interface Origin{
int sum(int a, int b);
}
更有甚者,參數的類型也可以省略,他會自己去匹配:
//省略參數類型
Origin o1 = (a, b) -> {
return a+b;
};
如果實現接口的方法,只有一行代碼,甚至花括號也可以省略:
Origin o2 = (a, b) -> a+b;
有關返回值和參數的個數還是有一些細微差別的。
Lambda表達式也在 Sort 方法里有應用,要想對引用類型里面統一按照某個屬性進行排序,需要實現Comparator接口里面的compare方法,可以使用簡化寫法。
- Lambda 表達式的支持,主要是為了避免匿名內部類定義過多,實質上是屬於函數式編程的概念。
- 需要注意的是,Lambda表達式只支持實現一個方法。