二十四条舔、多線程基礎(chǔ)
1. 線程簡介
世間萬物可以同一時間內(nèi)完成多件事堆巧,例如人可以同時看書和聽歌虚婿,這種思想運用在JAVA中被稱為并發(fā),而將并發(fā)完成的每一件事情稱為線程插勤。
在JAVA中沽瘦,并發(fā)機制非常重要革骨,但并不是所有的程序語言都支持線程。在以往的程序中析恋,多以一個任務(wù)完成后再進行下一個項目的模式進行開發(fā)良哲,這樣下一個任務(wù)的開始必須等待前一個任務(wù)的結(jié)束。JAVA語言提供了并發(fā)機制助隧,程序員可以在程序中執(zhí)行多個線程筑凫,每一個線程完成一個功能,并與其他線程并發(fā)執(zhí)行并村,這種機制被稱為多線程巍实。
-
Windows操作系統(tǒng)是多任務(wù)操作系統(tǒng),它以進程為單位橘霎。一個進程是一個包含有自身地址的程序蔫浆,每個獨立執(zhí)行的程序被稱為進程殖属,也就是正在執(zhí)行的程序姐叁。系統(tǒng)可以分配給每個進程一段有限的使用CPU的時間(也可以稱為CPU時間),CPU在這段時間中執(zhí)行某個進程洗显,然后下一個時間片又跳至另一個進程中去執(zhí)行外潜。由于CPU轉(zhuǎn)換較快,所以使得每個進程好像是同時執(zhí)行一樣挠唆。
一個線程則是進程中的執(zhí)行流程处窥,一個進程中可以同時包括多個線程,每個線程也可以得到一小段程序的執(zhí)行時間玄组,這樣一個進程就可以具有多個并發(fā)執(zhí)行的線程滔驾。在單線程中,程序代碼按照調(diào)用順序依次往下執(zhí)行俄讹,如果需要一個進程同時完成多段代碼的操作哆致,就需要多線程。
2. 實現(xiàn)線程的兩種方式
- 在JAVA中主要提供兩種方式實現(xiàn)線程患膛,分別為繼承java.lang.Thread類與實現(xiàn)java.lang.Runnable接口摊阀。
繼承Thread類
- Thread類是java.lang包中的一個類,從這個類中Thre實例化的對象代表線程踪蹬,若要啟動一個新線程則需要建立Thread實例胞此。Thread類中常用的兩個構(gòu)造方法如下:
public Thread():創(chuàng)建一個新的線程對象
public Thread(String threadName):創(chuàng)建一個名稱為threadName的線程對象
- 完成線程真正功能的代碼放在類的run()方法中,當一個類繼承Thread類后跃捣,就可以在該類中覆蓋run()方法漱牵,將實現(xiàn)該線程功能的代碼寫入run()方法中,然后同時調(diào)用Thread類中的start()方法執(zhí)行線程疚漆,也就是調(diào)用run()方法酣胀。
下面看一個案例:
- 在控制臺處打印出數(shù)字蚊惯,先使用單線程方式打印。
package com.lzw;
public class dxc1
{
public static void main(String[] args)
{
Thread a=new threadA();
a.start();
}
}
class threadA extends Thread
{ @Override
public void run()
{
for (int i=0;i<100;i++)
{
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結(jié)果:
- 由運行結(jié)果可以看到灵临,單線程工作時與之前的效果完全一致截型,也是一個數(shù)字一個數(shù)字地打印出來。下面使用多線程工作方式在控制臺處打印出數(shù)字和字母:
package com.lzw;
public class dxc1
{
public static void main(String[] args)
{
Thread a=new threadA();
a.start();
Thread b=new threadB();
b.start();
}
}
class threadA extends Thread
{ @Override
public void run()
{
for (int i=0;i<100;i++)
{
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class threadB extends Thread
{ @Override
public void run()
{
for (char j='a';j<'z';j++)
{
System.out.println(j);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結(jié)果:
- 由運行結(jié)果可以看到儒溉,使用了多線程(該例子使用了兩個線程)之后宦焦,控制臺處的數(shù)字和字母是同時打印出來的。
實現(xiàn)Runnable接口
-
到目前為止顿涣,線程都是通過繼承Thread類來創(chuàng)建的波闹,如果需要繼承其他類,而且還要使當前類實現(xiàn)多線程涛碑,那么可以通過Runnable接口來實現(xiàn)精堕。例如,一個擴展JFrame類的GUI程序不可能再繼承Thread類蒲障,因為JAVA語言不支持多繼承歹篓,這時候就需要Runnable接口使其具有使用多線程的功能。
- 創(chuàng)建一個繼承JFrame類的類揉阎,實現(xiàn)圖標移動的功能庄撮,其中使用了Swing與線程結(jié)合的技術(shù):
package lianxi1;
import java.awt.*;
import java.net.*;
import javax.swing.*;
public class SwingAndThread extends JFrame {
private static final long serialVersionUID = 1L;
private JLabel jl = new JLabel(); // 聲明JLabel對象
private static Thread t; // 聲明線程對象
private int count = 0; // 聲明計數(shù)變量
private Container container = getContentPane(); // 聲明容器
public SwingAndThread() {
setBounds(300, 200, 250, 100); // 絕對定位窗體大小與位置
container.setLayout(null); // 使窗體不使用任何布局管理器
URL url = SwingAndThread.class.getResource("/1.gif"); // 獲取圖片的URL
Icon icon = new ImageIcon(url); // 實例化一個Icon
jl.setIcon(icon); // 將圖標放置在標簽中
// 設(shè)置圖片在標簽的最左方
jl.setHorizontalAlignment(SwingConstants.LEFT);
jl.setBounds(10, 10, 200, 50); // 設(shè)置標簽的位置與大小
jl.setOpaque(true);
t = new Thread(new Runnable() { // 定義匿名內(nèi)部類,該類實現(xiàn)Runnable接口
public void run() { // 重寫run()方法
while (count <= 200) { // 設(shè)置循環(huán)條件
// 將標簽的橫坐標用變量表示
jl.setBounds(count, 10, 200, 50);
try {
Thread.sleep(1000); // 使線程休眠1000毫秒
} catch (Exception e) {
e.printStackTrace();
}
count += 4; // 使橫坐標每次增加4
if (count == 200) {
// 當圖標到達標簽的最右邊毙籽,使其回到標簽最左邊
count = 10;
}
}
}
});
t.start(); // 啟動線程
container.add(jl); // 將標簽添加到容器中
setVisible(true); // 使窗體可視
// 設(shè)置窗體的關(guān)閉方式
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
public static void main(String[] args) {
new SwingAndThread(); // 實例化一個SwingAndThread對象
}
}
運行結(jié)果:
- 在本實例中洞斯,為了使圖標具有滾動功能,需要在類的構(gòu)造方法中創(chuàng)建Thread實例坑赡。在創(chuàng)建該類的同時需要Runnable對象作為Thread類構(gòu)造方法的參數(shù)烙如,然后使用內(nèi)部類形式實現(xiàn)run()方法。在run()方法中主要循環(huán)圖標的橫坐標位置毅否,當圖標橫坐標到達標簽最右邊時亚铁,再次將圖標的橫坐標置于滾動的初始位置。
3. 線程的生命周期
-
線程具有生命周期搀突,其中包含了七種狀態(tài)刀闷、分別為出生狀態(tài)、就緒狀態(tài)仰迁、運行狀態(tài)甸昏、等待狀態(tài)、休眠狀態(tài)徐许、阻塞狀態(tài)和死亡狀態(tài)施蜜。出生狀態(tài)就是線程被創(chuàng)建時處于的狀態(tài),在用戶使用該線程實例調(diào)用start()方法之前線程都處于出生狀態(tài)雌隅;當用戶調(diào)用start()方法后翻默,線程就處于就緒狀態(tài)缸沃;當線程得到系統(tǒng)資源后就進入運行狀態(tài)。一旦線程進入可執(zhí)行狀態(tài)修械,它就會在就緒與運行狀態(tài)下轉(zhuǎn)換趾牧,同時也有可能進入等待、休眠肯污、阻塞或死亡狀態(tài)翘单。
4. 操作線程的方法
線程的休眠
- 一種能控制線程行為的方法是調(diào)用sleep()方法,sleep()方法需要一個參數(shù)用于指定該線程休眠的時間蹦渣,該時間以毫秒為單位哄芜。實際上線程的休眠的作用相當于暫停。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 由于sleep()方法的執(zhí)行有可能拋出InterruptedException異常柬唯,所以將sleep()方法放在try-catch塊中认臊。
線程的加入
- 如果當前某程序為多線程程序,假設(shè)存在一個線程A锄奢,現(xiàn)在需要插入線程B失晴,并要求線程B先執(zhí)行完畢,然后再繼續(xù)執(zhí)行線程A斟薇,此時可以使用Thread類中的join()方法來完成师坎。
- 創(chuàng)建一個繼承JFrame類的類恕酸,該實例包括兩個進度條堪滨,進度條的進度由線程來控制,通過使用join()方法使上面的進度條必須等待下面的進度條完成后才可以繼續(xù)蕊温。
package lianxi1;
import java.awt.*;
import javax.swing.*;
public class JoinTest extends JFrame {
private static final long serialVersionUID = 1L;
private Thread threadA; // 定義兩個線程
private Thread threadB;
final JProgressBar progressBar = new JProgressBar(); // 定義兩個進度條組件
final JProgressBar progressBar2 = new JProgressBar();
int count = 0;
public static void main(String[] args) {
init(new JoinTest(), 100, 100);
}
public JoinTest() {
super();
// 將進度條設(shè)置在窗體最北面
getContentPane().add(progressBar, BorderLayout.NORTH);
// 將進度條設(shè)置在窗體最南面
getContentPane().add(progressBar2, BorderLayout.SOUTH);
progressBar.setStringPainted(true); // 設(shè)置進度條顯示數(shù)字字符
progressBar2.setStringPainted(true);
// 使用匿名內(nèi)部類形式初始化Thread實例子
threadA = new Thread(new Runnable() {
int count = 0;
public void run() { // 重寫run()方法
while (true) {
progressBar.setValue(++count); // 設(shè)置進度條的當前值
try {
Thread.sleep(100); // 使線程A休眠100毫秒
threadB.join(); // 使線程B調(diào)用join()方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
threadA.start(); // 啟動線程A
threadB = new Thread(new Runnable() {
int count = 0;
public void run() {
while (true) {
progressBar2.setValue(++count); // 設(shè)置進度條的當前值
try {
Thread.sleep(100); // 使線程B休眠100毫秒
} catch (Exception e) {
e.printStackTrace();
}
if (count == 100) // 當count變量增長為100時
break; // 跳出循環(huán)
}
}
});
threadB.start(); // 啟動線程B
}
// 設(shè)置窗體各種屬性方法
public static void init(JFrame frame, int width, int height) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setVisible(true);
}
}
運行結(jié)果:
- 這個例子創(chuàng)建了兩個線程袱箱,這兩個線程分別負責進度條的滾動。在線程A的run()方法中使線程B的對象調(diào)用join()方法义矛,而join()方法使當前運行線程暫停发笔,直到調(diào)用join()方法的線程執(zhí)行完畢后再執(zhí)行,所以線程A等待線程B執(zhí)行完畢后再開始執(zhí)行凉翻,即下面的進度條滾動完畢后上面的進度條才開始滾動了讨。
線程的中斷
- 使用Thread類的interrupt()方法使線程離開run()方法,同時結(jié)束線程制轰,但是線程會拋出InterruputedException異常前计,用戶可以在處理異常時完成線程的中斷業(yè)務(wù)處理。
package lianxi1;
import java.awt.*;
import javax.swing.*;
public class InterruptedSwing extends JFrame {
private static final long serialVersionUID = 1L;
Thread thread;
public static void main(String[] args) {
init(new InterruptedSwing(), 100, 100);
}
public InterruptedSwing() {
super();
final JProgressBar progressBar = new JProgressBar(); // 創(chuàng)建進度條
// 將進度條放置在窗體合適位置
getContentPane().add(progressBar, BorderLayout.NORTH);
progressBar.setStringPainted(true); // 設(shè)置進度條上顯示數(shù)字
thread = new Thread(new Runnable() {
int count = 0;
public void run() {
while (true) {
progressBar.setValue(++count); // 設(shè)置進度條的當前值
try {
Thread.sleep(1000); // 使線程休眠1000豪秒
// 捕捉InterruptedException異常
} catch (InterruptedException e) {
System.out.println("當前線程序被中斷");
break;
}
}
}
});
thread.start(); // 啟動線程
thread.interrupt(); // 中斷線程
}
public static void init(JFrame frame, int width, int height) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setVisible(true);
}
}
運行結(jié)果:
5. 線程同步
- 在單線程程序中垃杖,每次只能做一件事男杈,后面的事情需要等待前面的事情完成之后才可以進行,但是如果使用多線程调俘,就會發(fā)生兩個線程搶占資源的問題伶棒,甚至在某些情況會產(chǎn)生臟數(shù)據(jù)旺垒。所以在多線程編程過程中需要防止這些資源訪問的沖突肤无。JAVA提供了線程同步的機制來防止資源訪問的沖突先蒋。
線程安全
- 如果線程不同步的話就會造成線程的不安全宛渐,例如火車票售賣系統(tǒng)鞭达,如果造成線程的不安全,則可能會出現(xiàn)車票數(shù)為負數(shù)的情況:
package lianxi1;
public class ThreadSafeTest implements Runnable {
int num = 10; // 設(shè)置當前總票數(shù)
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("剩余票數(shù)" + num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest(); // 實例化類對象
Thread tA = new Thread(t); // 以該類對象分別實例化4個線程
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start(); // 分別啟動線程
tB.start();
tC.start();
tD.start();
}
}
運行結(jié)果:
- 可以看到剩余車票數(shù)出現(xiàn)了負數(shù)的情況皇忿,這就出現(xiàn)了線程不安全的情況畴蹭。
線程同步機制
- 在JAVA中提供了同步機制,可以有效地防止資源沖突鳍烁。同步機制用synchronized關(guān)鍵字叨襟。
package lianxi1;
public class ThreadSafeTest implements Runnable {
int num = 10;
public void run() {
while (true) {
synchronized ("") {
if (num > 0) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("剩余票數(shù)" + --num);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest t = new ThreadSafeTest();
Thread tA = new Thread(t);
Thread tB = new Thread(t);
Thread tC = new Thread(t);
Thread tD = new Thread(t);
tA.start();
tB.start();
tC.start();
tD.start();
}
}