程序在沒有流程控制的前提下,代碼都是從上而下逐行依次執(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ǔ)空間(堆、??臻g)是獨(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");
}
- 通過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ì)象的名稱就可以這樣來獲?。?code>String name = Thread.currentThread().getName();。
通過查看該案例的打印結(jié)果,不難發(fā)現(xiàn):三個(gè)小朋友一共搶到了50個(gè)氣球,符合了需求中規(guī)氣球總共有50個(gè)的要求。我們?cè)賮矸治鲋骱瘮?shù)中的代碼,發(fā)現(xiàn)是因?yàn)?code>3個(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è)資源;
在下一篇文章中,我會(huì)繼續(xù)使用上述的案例來分析線程不安全以及相關(guān)的解決方法,敬請(qǐng)期待。
完結(jié)。老夫雖不正經(jīng),但老夫一身的才華