多線程的概述
- 進(jìn)程
- 正在運(yùn)行的程序,是系統(tǒng)進(jìn)行資源分配和調(diào)用的獨(dú)立單位。
- 每一個(gè)進(jìn)程都有它自己的內(nèi)存空間和系統(tǒng)資源。
說起線程,它又分為單線程和多線程
- 線程
- 是進(jìn)程中的單個(gè)順序控制流,是一條執(zhí)行路徑
- 一個(gè)進(jìn)程如果只有一條執(zhí)行路徑,則稱為單線程程序
- 一個(gè)進(jìn)程如果有多條執(zhí)行路徑,則稱為多線程程序
多線程的實(shí)現(xiàn)(1)
如何實(shí)現(xiàn)多線程的程序呢?
由于線程是依賴進(jìn)程而存在的,所以我們應(yīng)該先創(chuàng)建一個(gè)進(jìn)程出來。而進(jìn)程是由系統(tǒng)創(chuàng)建的,所以我們應(yīng)該去調(diào)用系統(tǒng)功能創(chuàng)建一個(gè)進(jìn)程。Java是不能直接調(diào)用系統(tǒng)功能的,所以,我們沒有辦法直接實(shí)現(xiàn)多線程程序。但是呢?Java可以去調(diào)用C/C++寫好的程序來實(shí)現(xiàn)多線程程序。由C/C++去調(diào)用系統(tǒng)功能創(chuàng)建進(jìn)程,然后由Java去調(diào)用這樣的東西,然后提供一些類供我們使用。我們就可以實(shí)現(xiàn)多線程程序了。
-
方式1:繼承Thread類
- 步驟
A:自定義類MyThread繼承Thread類。
B:MyThread類里面重寫run()
C:創(chuàng)建對(duì)象
D:啟動(dòng)線程
- 步驟
下面我們就自定義一個(gè)MyThread類繼承Thread類啟動(dòng)線程
public class MyThread extends Thread {
@Override
public void run() {
// 自己寫代碼
// 一般來說,被線程執(zhí)行的代碼肯定是比較耗時(shí)的。所以我們用循環(huán)改進(jìn)
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 創(chuàng)建兩個(gè)線程對(duì)象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
這樣我們就創(chuàng)建并啟動(dòng)了兩個(gè)線程
start()方法:首先啟動(dòng)了線程,然后再由jvm去調(diào)用該線程的run()方法。
那么,我們?cè)诶^承Thread類之后,為什么要重寫run()方法呢?
- 因?yàn)椴皇穷愔械乃写a都需要被線程執(zhí)行的。而這個(gè)時(shí)候,為了區(qū)分哪些代碼能夠被線程執(zhí)行,java提供了Thread類中的run()用來包含那些被線程執(zhí)行的代碼。
獲取和設(shè)置線程名稱
- Thread類的基本獲取和設(shè)置方法
- public final String getName():獲取線程的名稱。
- public final void setName(String name):設(shè)置線程的名稱
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 創(chuàng)建線程對(duì)象
//無參構(gòu)造+setXxx()
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//調(diào)用方法設(shè)置名稱
my1.setName("阿杜");
my2.setName("杜鵬程");
my1.start();
my2.start();
//帶參構(gòu)造方法給線程起名字
// MyThread my1 = new MyThread("阿杜");
// MyThread my2 = new MyThread("杜鵬程");
// my1.start();
// my2.start();
//我們可以使用無參構(gòu)造的方法,也可以使用帶參構(gòu)造的方法
}
}
但是我們要獲取main方法所在的線程對(duì)象的名稱,該怎么辦呢?
遇到這種情況,Thread類提供了一個(gè)很好玩的方法:
public static Thread currentThread():返回當(dāng)前正在執(zhí)行的線程對(duì)象
System.out.println(Thread.currentThread().getName());
這句話如果在main中執(zhí)行,就會(huì)輸出main。會(huì)返回當(dāng)前執(zhí)行的線程對(duì)象
線程控制
- public static void sleep(long millis):線程休眠
- public final void join():線程加入
- public static void yield():線程禮讓
- public final void setDaemon(boolean on):后臺(tái)線程
- public final void stop():中斷線程
- public void interrupt():中斷線程
public static void sleep(long millis):線程休眠
public class ThreadSleep extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x + ",日期:" + new Date());
// 睡眠1秒鐘
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ts1.setName("阿杜");
ts2.setName("杜鵬程");
ts1.start();
ts2.start();
}
}
public final void join():線程加入,等待該線程終止
public class ThreadJoin extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("中秋節(jié)");
tj2.setName("國慶節(jié)");
tj3.setName("圣誕節(jié)");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
運(yùn)行程序,我們發(fā)現(xiàn)名字為中秋節(jié)的線程走完了之后才開始走下面的兩個(gè)線程。
給那個(gè)線程用這個(gè)方法就是等待該線程終止后,再繼續(xù)執(zhí)行接下來的線程。
public static void yield():線程禮讓
public class ThreadYield extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
Thread.yield();
}
}
}
public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();
ty1.setName("阿杜");
ty2.setName("杜鵬程");
ty1.start();
ty2.start();
}
}
這個(gè)方法暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。
讓多個(gè)線程的執(zhí)行更和諧,但是不能靠它保證一人一次。
public final void setDaemon(boolean on):守護(hù)線程
public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("關(guān)羽");
td2.setName("張飛");
// 設(shè)置守護(hù)線程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
Thread.currentThread().setName("劉備");
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
運(yùn)行程序可以看到,當(dāng)劉備執(zhí)行完5次后,張飛和關(guān)于也會(huì)執(zhí)行完,并不會(huì)執(zhí)行100次。
將該線程標(biāo)記為守護(hù)線程或用戶線程。
當(dāng)正在運(yùn)行的線程都是守護(hù)線程時(shí),Java 虛擬機(jī)退出。
該方法必須在啟動(dòng)線程前調(diào)用。
**public final void stop():中斷線程 **
public void interrupt():中斷線程
這兩個(gè)方法都是中斷線程的意思,但是他們還是有區(qū)別的,我們來一起研究一下
public class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("開始執(zhí)行:" + new Date());
// 我要休息10秒鐘,親,不要打擾我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("線程被終止了");
}
System.out.println("結(jié)束執(zhí)行:" + new Date());
}
}
public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();
// 你超過三秒不醒過來,我就干死你
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我們分別運(yùn)行stop()方法和interrupt()方法。
我們可以發(fā)現(xiàn)stop()方法執(zhí)行后,該線程就停止了,不再繼續(xù)執(zhí)行了
但是interrupt()方法執(zhí)行后,它會(huì)終止線程的狀態(tài),還會(huì)繼續(xù)執(zhí)行run方法里面的代碼。
線程的生命周期圖

多線程的實(shí)現(xiàn)(2)
- 方式2:實(shí)現(xiàn)Runnable接口
- 步驟:
- A:自定義類MyRunnable實(shí)現(xiàn)Runnable接口
- B:重寫run()方法
- C:創(chuàng)建MyRunnable類的對(duì)象
- D:創(chuàng)建Thread類的對(duì)象,并把C步驟的對(duì)象作為構(gòu)造參數(shù)傳遞
- 步驟:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于實(shí)現(xiàn)接口的方式就不能直接使用Thread類的方法了,但是可以間接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
// 創(chuàng)建MyRunnable類的對(duì)象
MyRunnable my = new MyRunnable();
// 創(chuàng)建Thread類的對(duì)象,并把C步驟的對(duì)象作為構(gòu)造參數(shù)傳遞
// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "阿杜");
Thread t2 = new Thread(my, "杜鵬程");
t1.start();
t2.start();
}
}
這樣我們就實(shí)現(xiàn)了多線程的第二種啟動(dòng)方式
多線程程序練習(xí)
某電影院目前正在上映賀歲大片,共有100張票,而它有3個(gè)售票窗口售票,請(qǐng)?jiān)O(shè)計(jì)一個(gè)程序模擬該電影院售票。
我們分別用兩種實(shí)現(xiàn)多線程的方法來完成這個(gè)需求
1.繼承Thread類來實(shí)現(xiàn)
public class SellTicket extends Thread {
// 定義100張票
private static int tickets = 100;
@Override
public void run() {
// 定義100張票
// 每個(gè)線程進(jìn)來都會(huì)走這里,這樣的話,每個(gè)線程對(duì)象相當(dāng)于買的是自己的那100張票,這不合理,所以應(yīng)該定義到外面
// int tickets = 100;
// 是為了模擬一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" +(tickets--) + "張票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 創(chuàng)建三個(gè)線程對(duì)象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();
// 給線程對(duì)象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
// 啟動(dòng)線程
st1.start();
st2.start();
st3.start();
}
}
這樣我們就實(shí)現(xiàn)了三個(gè)窗口同時(shí)在出售這100張票的多線程程序
2.實(shí)現(xiàn)Runnable接口的方式實(shí)現(xiàn)
public class SellTicket implements Runnable {
// 定義100張票
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 創(chuàng)建資源對(duì)象
SellTicket st = new SellTicket();
// 創(chuàng)建三個(gè)線程對(duì)象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 啟動(dòng)線程
t1.start();
t2.start();
t3.start();
}
}
我們這個(gè)電影院售票程序,從表面上看不出什么問題,但是在真實(shí)生活中,售票時(shí)網(wǎng)絡(luò)是不能實(shí)時(shí)傳輸?shù)?,總是存在延遲的情況,所以,在出售一張票以后,需要一點(diǎn)時(shí)間的延遲,所以我們每次賣票延遲100毫秒
while (true) {
if (tickets > 0) {
// 為了模擬更真實(shí)的場(chǎng)景,我們稍作休息
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
}
}
}
這樣我們模擬真是場(chǎng)景,稍作休息了,可是運(yùn)行程序后,還是會(huì)出現(xiàn)下面兩個(gè)問題。
- 相同的票出現(xiàn)多次
- CPU的一次操作必須是原子性的 - 還出現(xiàn)了負(fù)數(shù)的票
- 隨機(jī)性和延遲導(dǎo)致的
這里就牽扯到了線程的安全問題,線程安全問題在理想狀態(tài)下,不容易出現(xiàn),但一旦出現(xiàn)對(duì)軟件的影響是非常大的。
多線程安全問題
如何解決多線程安全問題呢?
- 把多個(gè)語句操作共享數(shù)據(jù)的代碼給鎖起來,讓任意時(shí)刻只能有一個(gè)線程執(zhí)行即可。
解決線程安全問題實(shí)現(xiàn)(1)
- 同步代碼塊
- 格式:
- synchronized(對(duì)象){ 需要同步的代碼; }
- 同步可以解決安全問題的根本原因就在那個(gè)對(duì)象上。該對(duì)象如同鎖的功能。
- 格式:
我們多上面售票的代碼進(jìn)行改進(jìn)
public class SellTicket implements Runnable {
// 定義100張票
private int tickets = 100;
//創(chuàng)建鎖對(duì)象
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票");
}
}
}
}
}
我們只要運(yùn)用同步代碼塊的格式來解決線程的問題就可以,主要就是這里的對(duì)象,必須使用的是同一個(gè)鎖對(duì)象。
所以我們可以來總結(jié)一下同步的特點(diǎn)
同步的特點(diǎn)
- 同步的前提
- 多個(gè)線程
- 多個(gè)線程使用的是同一個(gè)鎖對(duì)象
- 同步的好處
- 同步的出現(xiàn)解決了多線程的安全問題。
- 同步的弊端
- 當(dāng)線程相當(dāng)多時(shí),因?yàn)槊總€(gè)線程都會(huì)去判斷同步上的鎖,這是很耗費(fèi)資源的,無形中會(huì)降低程序的運(yùn)行效率。
解決線程安全問題實(shí)現(xiàn)(2)
我們 還有一種方法可以解決多線程的安全問題
同步方法:就是把同步的關(guān)鍵字加到方法上
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
我們只要調(diào)用這個(gè)方法就可以了
我們也可以讓此方法為靜態(tài)的方法
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "張票 ");
}
}
我們要來總結(jié)一下,同步代碼塊的鎖對(duì)象可以時(shí)任意對(duì)象。
但是,當(dāng)把同步關(guān)鍵字加在方法上,它的對(duì)象是this
當(dāng)此方法為精態(tài)方法時(shí),它的對(duì)象是類的字節(jié)碼文件對(duì)象,也就是 類名.class