程序在沒有流程控制的前提下疗我,代碼都是從上而下逐行依次執(zhí)行的咆畏。基于這樣的機(jī)制吴裤,如果我們使用程序來實(shí)現(xiàn)邊打游戲旧找,邊聽音樂的需求時(shí),就會(huì)很困難麦牺;因?yàn)榘凑請(qǐng)?zhí)行順序钮蛛,只能從上往下依次執(zhí)行;同一時(shí)刻剖膳,只能執(zhí)行聽音樂和打游戲的其中之一魏颓。
為了解決這樣的問題,在程序設(shè)計(jì)中引入了多線程并發(fā)吱晒。本文中的知識(shí)對(duì)windows
甸饱、mac
、linux
系統(tǒng)都適用仑濒,但展示界面和功能名稱上不太一樣叹话;相關(guān)的截圖這里以windows
為例。
并行和并發(fā)
并行和并發(fā)是兩個(gè)很容易混淆的概念墩瞳,他們?cè)谧置嫔侠斫馄饋砜赡軟]有很大的差異驼壶,但要放在計(jì)算機(jī)運(yùn)行環(huán)境中來解釋,兩者是有很大區(qū)別的:
- 并行:多個(gè)事件在同一個(gè)時(shí)間點(diǎn)同時(shí)發(fā)生矗烛,是真正的同時(shí)發(fā)生辅柴;
- 并發(fā):多個(gè)事件在同一時(shí)間段內(nèi)在宏觀上同時(shí)發(fā)生,而在微觀上是CPU在多個(gè)事件上來回切換瞭吃,但切換的時(shí)間很快碌嘀,并不能被人眼捕獲,因此在一段人類可以觀察到的時(shí)間內(nèi)歪架,多個(gè)事件是同時(shí)發(fā)生的股冗;
操作系統(tǒng)的運(yùn)行環(huán)境中,并發(fā)指的就是一段時(shí)間內(nèi)宏觀上多個(gè)程序在同時(shí)運(yùn)行和蚪;在單CPU
的環(huán)境中止状,微觀上每一個(gè)時(shí)刻僅有一個(gè)程序被CPU
執(zhí)行(也就是僅有一個(gè)程序獲得了CPU時(shí)間片
),CPU
是在多個(gè)程序之間來回交替執(zhí)行攒霹,也就是給每個(gè)程序的運(yùn)行時(shí)間進(jìn)行調(diào)度怯疤,從而實(shí)現(xiàn)多個(gè)程序的并發(fā)運(yùn)行。
隨著計(jì)算機(jī)硬件的不斷發(fā)展催束,現(xiàn)如今的計(jì)算機(jī)一般都是有多個(gè)CPU
的集峦,在這樣的多個(gè)CPU
的環(huán)境中,原本由單個(gè)處理器運(yùn)行的這些程序就可以被分配給多個(gè)CPU
來運(yùn)行,從而實(shí)現(xiàn)真程序的并行運(yùn)行塔淤,無論從宏觀上摘昌,還是微觀上,程序都是同時(shí)運(yùn)行的高蜂。這樣聪黎,程序的運(yùn)行效率就會(huì)大大提高。
PS:CPU時(shí)間片就是CPU分配給每個(gè)程序的運(yùn)行時(shí)間备恤。
在買電腦的時(shí)候稿饰,電腦廠商宣傳的“幾核處理器”,其中“核”表示的是CPU
有幾個(gè)物理核心烘跺,能夠并行處理幾個(gè)程序的調(diào)用湘纵。想要知道自己電腦是幾核的,可以打開“任務(wù)管理器”來查看滤淳。
也可以通過計(jì)算機(jī)屬性梧喷、設(shè)備管理器來查看。 所以脖咐,單核處理器是不能并行運(yùn)行多個(gè)任務(wù)的铺敌,只能是多個(gè)任務(wù)在單核處理器中并發(fā)運(yùn)行,我們把每個(gè)任務(wù)用一個(gè)線程來表示屁擅,多個(gè)線程在單個(gè)處理器中的并發(fā)運(yùn)行我們稱之為線程調(diào)度偿凭。
從宏觀上講,多個(gè)線程是并行運(yùn)行的派歌;從微觀上講弯囊,多個(gè)線程是串行運(yùn)行的,也就是一個(gè)線程一個(gè)線程的運(yùn)行胶果;如果對(duì)這里的宏觀和微觀不太好理解的話匾嘱,可以把宏觀看作是站在人的角度看待程序運(yùn)行,把微觀看作是站在CPU
的角度看待程序運(yùn)行早抠,這樣就好理解多了霎烙。
線程和進(jìn)程
進(jìn)程:進(jìn)程是指一個(gè)在內(nèi)存中運(yùn)行的應(yīng)用程序,每個(gè)進(jìn)程在內(nèi)存中都有一塊獨(dú)立的內(nèi)存空間蕊连。每個(gè)軟件都可以啟動(dòng)多個(gè)進(jìn)程悬垃。
線程:線程指的是進(jìn)程中的一個(gè)控制單元,也就是進(jìn)程中的每個(gè)單元任務(wù)甘苍,一個(gè)進(jìn)程中可以有多個(gè)線程同時(shí)并發(fā)運(yùn)行尝蠕。
多進(jìn)程指的是操作系統(tǒng)中同時(shí)運(yùn)行的多個(gè)程序,多線程指的是同一個(gè)進(jìn)程中同時(shí)運(yùn)行的多個(gè)任務(wù)载庭。操作系統(tǒng)中運(yùn)行的每個(gè)任務(wù)就是一個(gè)進(jìn)程看彼,進(jìn)程中的每個(gè)任務(wù)就是一個(gè)線程扇谣;操作系統(tǒng)就是一個(gè)多任務(wù)系統(tǒng),它可以有多個(gè)進(jìn)程闲昭,每個(gè)進(jìn)程又可以有多個(gè)線程。
線程和進(jìn)程的區(qū)別:
每個(gè)進(jìn)程都有獨(dú)立的內(nèi)存空間靡挥,也就是進(jìn)程中的數(shù)據(jù)存儲(chǔ)空間(堆序矩、棧空間)是獨(dú)立的跋破,且每個(gè)進(jìn)程都至少有一個(gè)線程簸淀;
對(duì)于線程來說:堆內(nèi)存空間是共享的,棧內(nèi)存空間是獨(dú)立的毒返;線程消耗的資源比進(jìn)程要小得多租幕,且線程之間是可以相互通信的;
線程是進(jìn)程的基本組成單元拧簸,故也把線程稱作進(jìn)程元劲绪,或者輕型進(jìn)程;
線程的執(zhí)行是通過
CPU
調(diào)度器來決定的盆赤,程序員無法控制贾富;
線程調(diào)度: 計(jì)算機(jī)單個(gè)CPU
在任意時(shí)刻只能執(zhí)行一條計(jì)算機(jī)指令,每個(gè)進(jìn)程只有獲得CPU
使用權(quán)才能執(zhí)行相關(guān)指令牺六;
多線程并發(fā)颤枪,其實(shí)就是運(yùn)行中各個(gè)進(jìn)程輪流獲取CPU
的使用權(quán)來分別執(zhí)行各自的任務(wù);在多進(jìn)程的環(huán)境中淑际,會(huì)有多個(gè)線程處于等待獲取CPU
使用權(quán)的狀態(tài)中畏纲,為這些等待中的線程分配CPU
使用權(quán)的操作就成為線程調(diào)度。線程調(diào)度分為搶占式調(diào)度和分時(shí)調(diào)度春缕。
搶占式調(diào)度:多個(gè)線程在瞬間搶占
CPU
資源盗胀,誰搶到誰就運(yùn)行,有更多的隨機(jī)性淡溯;分時(shí)調(diào)度:為等待中的多個(gè)線程平均的分配
CPU
時(shí)間片读整;
Java
的多線程中線程調(diào)度就是使用搶占式調(diào)度的。
多線程
多線程和單線程咱娶,就好比多行道和單行道米间,多行道可以有多輛車同時(shí)行駛通過,而單行道只能是多輛車按順序依次行駛通過膘侮;多線程同時(shí)有多個(gè)線程并發(fā)運(yùn)行屈糊,單線程只有單個(gè)線程對(duì)多個(gè)任務(wù)按順序依次執(zhí)行。
如果以下載文件為例:單線程就是只有一個(gè)文件下載的通道琼了,多線程則是同時(shí)有多個(gè)下載通道在下載文件逻锐。當(dāng)服務(wù)器提供下載服務(wù)時(shí)夫晌,下載程序是共享服務(wù)器帶寬的,在優(yōu)先級(jí)相同的情況下昧诱,服務(wù)器會(huì)對(duì)下載中的所有線程平均分配帶寬:
寬帶帶寬是以位(bit
)來計(jì)算的晓淀,而下載速度是以字節(jié)(byte
)計(jì)算的,1 byte = 8 bit
盏档,故1024KB/s
代表的是上網(wǎng)寬帶為1M
(1024
千位)凶掰,而下載速度需要用1024KB/s
除去8
,得出`128KB/s蜈亩。
多線程是為了同步完成多項(xiàng)任務(wù)懦窘,是為了提高系統(tǒng)整體的效率,而不能提高程序代碼自身的運(yùn)行效率稚配。
多線程的優(yōu)勢:多線程作為一種多任務(wù)畅涂、高并發(fā)的運(yùn)行機(jī)制,有其獨(dú)到的優(yōu)勢所在:
進(jìn)程之間不能共享內(nèi)存空間道川,但是線程之間是可以的(通過堆內(nèi)存)午衰;
創(chuàng)建進(jìn)程時(shí),操作系統(tǒng)需要為其重新分配系統(tǒng)資源愤惰;而創(chuàng)建線程耗費(fèi)的資源會(huì)小很多苇经,在實(shí)現(xiàn)多任務(wù)并發(fā)時(shí),相比較于多進(jìn)程宦言,多線程的效率會(huì)高很多扇单;
Java
語言內(nèi)置了對(duì)多線程的支持,而不僅僅是簡單的調(diào)用底層操作系統(tǒng)的調(diào)度奠旺;Java
對(duì)多線程的支持也很友好蜘澜,能大大簡化開發(fā)成本;
創(chuàng)建進(jìn)程
在Java
中創(chuàng)建進(jìn)程可通過兩種方式來實(shí)現(xiàn):
1. 通過java.lang.Runtime
來實(shí)現(xiàn)响疚,示例代碼如下:
public static void main(String []args) throws IOException {
// 方式一:通過通過java.lang.Runtime來實(shí)現(xiàn)打開 cmd
Runtime runtime = Runtime.getRuntime();
runtime.exec("cmd");
}
2. 通過java.lang.ProcessBuilder
來實(shí)現(xiàn)鄙信,示例代碼如下:
public static void main(String []args) throws IOException {
// 方式二:通過通過java.lang.ProcessBuilder來實(shí)現(xiàn)打開 cmd
ProcessBuilder pb = new ProcessBuilder("cmd");
pb.start();
}
創(chuàng)建線程
一、通過繼承Thread
類創(chuàng)建線程忿晕;需要注意的是:只有Thread
的子類才是線程類装诡;
新創(chuàng)建一個(gè)類繼承于
java.lang.Thread
;在新建的
Thread
子類中重寫Thread
類中的run
方法践盼,在run
方法中編寫線程邏輯鸦采;創(chuàng)建線程對(duì)象,執(zhí)行線程邏輯咕幻;
public class ExtendsThreadDemo {
public static void main(String []args) {
for (int i = 0; i < 50; i++) {
System.out.println("主線程" + i);
if (i == 13) {
NewThread newThread = new NewThread();
newThread.start();
}
}
}
}
// 新線程類
class NewThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("新線程" + i);
}
}
}
二渔伯、通過實(shí)現(xiàn)Runnable
接口創(chuàng)建線程;需要注意肄程,這里的Runnable
實(shí)現(xiàn)類并不是線程類锣吼,所以啟動(dòng)方式和Thread
子類會(huì)有所不同选浑;
- 新創(chuàng)建一個(gè)類實(shí)現(xiàn)
java.lang.Runnable
; - 在新建的實(shí)現(xiàn)類中重寫
Runnable
類中的run
方法玄叠,在run
方法中編寫線程邏輯古徒; - 創(chuàng)建
Thread
對(duì)象,傳入Runnable
實(shí)現(xiàn)類對(duì)象读恃,執(zhí)行線程邏輯描函;
示例代碼如下:
public class ImplementsRunnableDemo {
public static void main(String []args) {
for (int i = 0; i < 50; i++) {
System.out.println("主線程" + i);
if (i == 13) {
Runnable runnable = new NewRunnableImpl();
Thread thread = new Thread(runnable);
thread.start();
}
}
}
}
// 新線程類
class NewRunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("新線程" + i);
}
}
}
三、使用匿名內(nèi)部類創(chuàng)建線程狐粱,使用接口的匿名內(nèi)部類來創(chuàng)建線程,示例代碼如下:
// 使用接口的匿名內(nèi)部類
public class AnonymousInnerClassDemo {
public static void main(String []args) {
for (int i = 0; i < 50; i++) {
System.out.println("主線程" + i);
if (i == 13) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 50; j++) {
System.out.println("新線程" + j);
}
}
}).start();
}
}
}
}
當(dāng)然了胆数,也可以使用Thread
類的匿名內(nèi)部類創(chuàng)建線程肌蜻,不過這樣的方式很少使用;示例代碼如下:
// 使用Thread類的匿名內(nèi)部類
public class AnonymousInnerClassDemo {
public static void main(String []args) {
for (int i = 0; i < 50; i++) {
System.out.println("主線程" + i);
if (i == 13) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 50; j++) {
System.out.println("新線程" + j);
}
}
}.start();
}
}
}
}
多線程案例
案例需求:六一兒童節(jié)必尼,設(shè)置了搶氣球比賽節(jié)目蒋搜,共有50
個(gè)氣球,三個(gè)小朋友小紅判莉、小強(qiáng)豆挽、小明來搶;請(qǐng)使用多線程技術(shù)來實(shí)現(xiàn)上述比賽過程券盅。
一帮哈、使用繼承Thread
類的方式來實(shí)現(xiàn)上述案例;示例代碼如下:
public class ExtendsDemo {
public static void main(String []args) {
new Student("小紅").start();
new Student("小強(qiáng)").start();
new Student("小明").start();
}
}
class Student extends Thread {
private int num = 50;
private String name;
public Student(String name) {
super(name);
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (num > 0) {
System.out.println(this.name + "搶到了" + num + "號(hào)氣球");
num--;
}
}
}
}
通過查看輸出結(jié)果锰镀,發(fā)現(xiàn)一個(gè)問題:每個(gè)小朋友都搶到了50
個(gè)氣球娘侍,這和原本只有50
個(gè)氣球相矛盾了;不過別急泳炉,我們可以使用第二種方式:使用實(shí)現(xiàn)接口的方式來實(shí)現(xiàn)上述案例 來解決憾筏。
二、使用實(shí)現(xiàn)接口的方式來實(shí)現(xiàn)上述案例花鹅;示例代碼如下:
public class ImplementsDemo {
public static void main(String []args) {
Balloon balloon = new Balloon();
new Thread(balloon, "小紅").start();
new Thread(balloon, "小強(qiáng)").start();
new Thread(balloon, "小明").start();
}
}
// 氣球
class Balloon implements Runnable {
private int num = 50;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "搶到了"
+ num + "號(hào)氣球");
num--;
}
}
}
}
在該案例中我們是用了Thread.currentThread()
方法氧腰,該方法的作用是返回當(dāng)前正在執(zhí)行的線程對(duì)象的引用,所以當(dāng)前正在執(zhí)行的線程對(duì)象的名稱就可以這樣來獲扰偎唷:String name = Thread.currentThread().getName();
古拴。
通過查看該案例的打印結(jié)果,不難發(fā)現(xiàn):三個(gè)小朋友一共搶到了50
個(gè)氣球之景,符合了需求中規(guī)氣球總共有50
個(gè)的要求斤富。我們?cè)賮矸治鲋骱瘮?shù)中的代碼,發(fā)現(xiàn)是因?yàn)?個(gè)線程共享了一個(gè)Balloon
對(duì)象锻狗,該對(duì)象中的氣球數(shù)量就在50
個(gè)满力。
按照這樣的思路焕参,上述使用繼承Thread
類的方式中出現(xiàn)的問題就可以解決了。接下來就對(duì)上述兩種實(shí)現(xiàn)多線程的方式進(jìn)行分析和總結(jié):
使用繼承Thread
類的方式:
- 使用繼承方式來實(shí)現(xiàn)多線程在操作上會(huì)更加簡便油额;比如:可以通過
super.getName()
來直接獲取當(dāng)前線程對(duì)象的名稱叠纷; - 由于
Java
是單繼承的,所以如果繼承了Thread
潦嘶,該類就不能再有其他的父類了涩嚣; - 對(duì)于搶氣球案例需求來說,并不能很好的解決問題掂僵;
使用實(shí)現(xiàn)接口的方式:
- 相較于繼承方式航厚,實(shí)現(xiàn)方式和線程操作會(huì)稍加復(fù)雜;比如:獲取當(dāng)前線程名稱需要通過
Thread.currentThread().getName();
來獲让膛睢幔睬; - 由于是使用實(shí)現(xiàn)的方式,
Java
是支持多實(shí)現(xiàn)的芹扭,所以除了Runnable
接口之外麻顶,還可以實(shí)現(xiàn)其他的接口,繼承另外的類舱卡; - 能夠很好的實(shí)現(xiàn)案例需求:多個(gè)線程共享一個(gè)資源辅肾;
完結(jié),老夫雖不正經(jīng)轮锥,但老夫一身的才華矫钓!關(guān)注我,獲取更多編程基礎(chǔ)知識(shí)舍杜。