線程是程序執(zhí)行的一條路徑, 一個(gè)進(jìn)程中可以包含多條線程。多線程并發(fā)執(zhí)行可以提高程序的效率,可以同時(shí)完成多項(xiàng)工作,多線程并發(fā)執(zhí)行的實(shí)質(zhì)就是CPU在做著高速的切換。多線程的應(yīng)用場(chǎng)景:紅蜘蛛同時(shí)共享屏幕給多個(gè)電腦;迅雷開啟多條線程一起下載;QQ同時(shí)和多個(gè)人一起視頻;服務(wù)器同時(shí)處理多個(gè)客戶端請(qǐng)求。
并行和并發(fā)的區(qū)別
- 并行就是兩個(gè)任務(wù)同時(shí)運(yùn)行,就是甲任務(wù)進(jìn)行的同時(shí),乙任務(wù)也在進(jìn)行。(需要多核CPU)
- 并發(fā)是指兩個(gè)任務(wù)都請(qǐng)求運(yùn)行,而處理器只能按受一個(gè)任務(wù),就把這兩個(gè)任務(wù)安排輪流進(jìn)行,由于時(shí)間間隔較短,使人感覺兩個(gè)任務(wù)都在運(yùn)行。
- 比如我跟兩個(gè)網(wǎng)友聊天,左手操作一個(gè)電腦跟甲聊,同時(shí)右手用另一臺(tái)電腦跟乙聊天,這就叫并行。如果用一臺(tái)電腦我先給甲發(fā)個(gè)消息,然后立刻再給乙發(fā)消息,然后再跟甲聊,再跟乙聊。這就叫并發(fā)。
Java程序運(yùn)行原理
Java命令會(huì)啟動(dòng)java虛擬機(jī),啟動(dòng)JVM,等于啟動(dòng)了一個(gè)應(yīng)用程序,也就是啟動(dòng)了一個(gè)進(jìn)程。該進(jìn)程會(huì)自動(dòng)啟動(dòng)一個(gè) “主線程” ,然后主線程去調(diào)用某個(gè)類的 main 方法。JVM啟動(dòng)至少啟動(dòng)了垃圾回收線程和主線程,所以是多線程的。
代碼驗(yàn)證:
public class Demo1_Thread { //此程序的運(yùn)行結(jié)果是 "我是主線程的執(zhí)行代碼"和"垃圾被清掃了"交替執(zhí)行
/**
證明jvm是多線程的
*/
public static void main(String[] args) {
for(int i = 0; i < 100000; i++) {
new Demo();
}
for(int i = 0; i < 10000; i++) {
System.out.println("我是主線程的執(zhí)行代碼");
}
}
}
class Demo {
@Override
public void finalize() {
System.out.println("垃圾被清掃了");
}
}
多線程的實(shí)現(xiàn)
方式一:定義類繼承Thread,重寫run方法,把新線程要做的事寫在run方法中;創(chuàng)建線程對(duì)象,開啟(調(diào)用start())新線程, 內(nèi)部會(huì)自動(dòng)執(zhí)行run方法。
代碼演示:
public class Demo2_Thread {
public static void main(String[] args) {
MyThread mt = new MyThread(); //4,創(chuàng)建Thread類的子類對(duì)象
mt.start(); //5,開啟線程
MyThread mt1 = new MyThread();
mt1.start();
for(int i = 0; i < 1000; i++) {
System.out.println("bb");
}
}
}
class MyThread extends Thread { //1,繼承Thread
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執(zhí)行的代碼寫在run方法中
System.out.println("aaaaaaaaaaaa");
}
}
}
方式二:定義類實(shí)現(xiàn)Runnable接口,實(shí)現(xiàn)run方法,把新線程要做的事寫在run方法中,創(chuàng)建自定義的Runnable的子類對(duì)象,創(chuàng)建Thread對(duì)象,傳入Runnable,調(diào)用start()開啟新線程,內(nèi)部會(huì)自動(dòng)調(diào)用Runnable的run()方法。
代碼演示:
public class Demo3_Thread {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable(); //4,創(chuàng)建Runnable的子類對(duì)象
Thread t = new Thread(mr); //5,將其當(dāng)作參數(shù)傳遞給Thread的構(gòu)造函數(shù)
t.start(); //6,開啟線程
for(int i = 0; i < 1000; i++) {
System.out.println("bb");
}
}
}
class MyRunnable implements Runnable { //1,定義一個(gè)類實(shí)現(xiàn)Runnable
@Override
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執(zhí)行的代碼寫在run方法中
System.out.println("aaaaaaaaaaaa");
}
}
}
剖析實(shí)現(xiàn)Runnable接口的源碼
源碼解釋:構(gòu)造函數(shù)中傳入了Runnable的引用,成員變量記住了它,start()調(diào)用run()方法時(shí)內(nèi)部判斷成員變量Runnable的引用是否為空,不為空編譯時(shí)看的是Runnable的run(),運(yùn)行時(shí)執(zhí)行的是子類的run()方法。
源碼解析:
public class Thread implements Runnable { //Thread類本身實(shí)現(xiàn)了Runnable接口
/*省略代碼*/
private Runnable target; //一個(gè)Runnable類型的成員變量
public Thread(Runnable target) {//Thread的有參構(gòu)造方法,傳入一個(gè)Runnable接口的子類對(duì)象target
//調(diào)用init方法(下面這個(gè)) 把target傳入
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
//init方法里面又調(diào)用另一個(gè)重載的init方法(下面這個(gè))把target傳入
init(g, target, name, stackSize, null);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
/*省略代碼*/
this.target = target;//在init方法里面把成員變量target 進(jìn)行賦值
/*省略代碼*/
}
public synchronized void start() { //當(dāng)調(diào)用start方法的時(shí)候 內(nèi)部調(diào)用的是start0()方法
/*省略代碼*/
start0();
/*省略代碼*/
}
private native void start0(); //start0()是native修飾的方法 底層是其他語(yǔ)言 我們無(wú)法查看,但是我們知道底層會(huì)調(diào)用run方法
@Override
public void run() { //start0()的底層是會(huì)調(diào)用run()方法
if (target != null) { //判斷成員變量target是否為null
target.run(); //如果target被賦值了,就要調(diào)用target的run方法
}
}
/*省略代碼*/
}
多線程兩種實(shí)現(xiàn)方式的區(qū)別
繼承Thread : 由于子類重寫了Thread類的run(),當(dāng)調(diào)用start()時(shí), 直接找子類的run()方法;好處是:可以直接使用Thread類中的方法,代碼簡(jiǎn)單;弊端是:如果已經(jīng)有了父類,就不能用這種方法。
實(shí)現(xiàn)Runnable : 構(gòu)造函數(shù)中傳入了Runnable的引用, 成員變量記住了它, start()調(diào)用run()方法時(shí)內(nèi)部判斷成員變量Runnable的引用是否為空, 不為空編譯時(shí)看的是Runnable的run(),運(yùn)行時(shí)執(zhí)行的是子類的run()方法。好處是:即使自己定義的線程類有了父類也沒(méi)關(guān)系,因?yàn)橛辛烁割愐部梢詫?shí)現(xiàn)接口,而且接口是可以多實(shí)現(xiàn)的;弊端是:不能直接使用Thread中的方法需要先獲取到線程對(duì)象后,才能得到Thread的方法,代碼復(fù)雜。
匿名內(nèi)部類實(shí)現(xiàn)線程的兩種方式
public static void main(String[] args) {
new Thread() { //1,繼承Thread類
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執(zhí)行的代碼寫在run方法中
System.out.println("aaaaaaaaaaaaaa");
}
}
}.start(); //4,開啟線程
new Thread(new Runnable() { //1,將Runnable的子類對(duì)象傳遞給Thread的構(gòu)造方法
public void run() { //2,重寫run方法
for(int i = 0; i < 1000; i++) { //3,將要執(zhí)行的代碼寫在run方法中
System.out.println("bb");
}
}
}).start(); //4,開啟線程
}
public final String getName():獲取線程對(duì)象的名稱。默認(rèn)情況下,名字的組成 Thread-編號(hào)(編號(hào)從0開始)
public final void setName(String name):設(shè)置線程名稱。
代碼演示:
//通過(guò)構(gòu)造方法給name賦值
public static void demo1() {
new Thread("芙蓉姐姐") { //通過(guò)構(gòu)造方法給name賦值
public void run() {
System.out.println(this.getName() + "....aaaaaaaaa");
}
}.start();
}
//通過(guò)setName()來(lái)設(shè)置線程的名字
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
//this.setName("張三"); //在這兒設(shè)置也行 在外面設(shè)置名稱也行
System.out.println(this.getName() + "....aaaaaaaaaaaaa");
}
};
t.setName("張三"); //通過(guò)setName()來(lái)設(shè)置線程的名字
t.start();
}
public static Thread currentThread():返回當(dāng)前正在執(zhí)行的線程對(duì)象引用
public static void sleep(long millis):讓當(dāng)前線程休眠millis毫秒
public final void setDaemon(boolean on):設(shè)置線程為守護(hù)線程,一旦前臺(tái)(主線程)結(jié)束,守護(hù)線程就結(jié)束了
public final void join():當(dāng)前線程暫停,等待指定的線程執(zhí)行結(jié)束后,當(dāng)前線程再繼續(xù)
public final void join(long millis):當(dāng)前線程暫停, 等待指定的線程執(zhí)行millis毫秒結(jié)束后, 當(dāng)前線程再繼續(xù)
public static void yield():暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。
public final int getPriority():獲取線程優(yōu)先級(jí)
public final void setPriority(int newPriority):更改線程的優(yōu)先級(jí)。線程默認(rèn)優(yōu)先級(jí)是5。范圍是1-10。
注意:優(yōu)先級(jí)可以在一定的程度上,讓線程獲較多的執(zhí)行機(jī)會(huì)。
代碼演示:
public static void main(String[] args) {
final Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName() + "...aaaaaaaaaaaaa");
}
}
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i < 10; i++) {
if(i == 2) {
try {
t1.join(); //t1插隊(duì)后,t1執(zhí)行結(jié)束后 t2才能接著執(zhí)行
//t1.join(1); //插隊(duì)指定的時(shí)間,過(guò)了指定時(shí)間后,兩條線程交替執(zhí)行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName() + "...bb");
}
}
};
t1.start();
t2.start();
}
同步代碼塊
當(dāng)多線程并發(fā), 有多段代碼同時(shí)執(zhí)行時(shí), 我們希望某一段代碼執(zhí)行的過(guò)程中CPU不要切換到其他線程工作,這時(shí)就需要同步。如果兩段代碼是同步的,那么同一時(shí)間只能執(zhí)行一段, 在一段代碼沒(méi)執(zhí)行結(jié)束之前, 不會(huì)執(zhí)行另外一段代碼。所謂同步代碼塊就是使用synchronized關(guān)鍵字加上一個(gè)鎖對(duì)象來(lái)定義一段代碼,這就叫同步代碼塊,多個(gè)同步代碼塊如果使用相同的鎖對(duì)象,那么他們就是同步的。
代碼演示
public class Demo_Synchronized {
public static void main(String[] args) {
//匿名內(nèi)部類使用局部變量,局部變量前面必須加final修飾,為了延長(zhǎng)局部變量的聲明周期
final Printer p = new Printer();
new Thread() {
public void run() {
while(true) {
p.print1(); //調(diào)用print1()
}
}
}.start();
new Thread() {
public void run() {
while(true) {
p.print2(); //調(diào)用print2()
}
}
}.start();
}
}
class Printer {
Demo d = new Demo();
public void print1() {
synchronized(d) { //同步代碼塊,鎖機(jī)制,鎖對(duì)象可以是任意的
//當(dāng)多線程并發(fā), 有多段代碼同時(shí)執(zhí)行時(shí)
//我們希望下面代碼執(zhí)行的過(guò)程中CPU不要切換到其他線程工作
System.out.print("黑");
System.out.print("馬");
System.out.print("程");
System.out.print("序");
System.out.print("員");
System.out.print("\r\n");
}
}
public void print2() {
synchronized(d) {//如果說(shuō)兩塊代碼想同步的話,那么這兩個(gè)同步代碼塊的鎖對(duì)象必須是同一個(gè)鎖對(duì)象,所以說(shuō)上面的鎖對(duì)象是d,這一個(gè)鎖對(duì)象也是d
//我們希望下面代碼執(zhí)行的過(guò)程中CPU不要切換到其他線程工作
System.out.print("傳");
System.out.print("智");
System.out.print("播");
System.out.print("客");
System.out.print("\r\n");
}
}
}
class Demo{}
同步方法
使用synchronized關(guān)鍵字修飾一個(gè)方法,該方法中所有的代碼都是同步的,非靜態(tài)同步函數(shù)的鎖是當(dāng)前對(duì)象this,靜態(tài)的同步函數(shù)的鎖是當(dāng)前類的字節(jié)碼對(duì)象。
代碼演示:
public class Demo_Synchronized {
public static void main(String[] args) {
//匿名內(nèi)部類使用局部變量,局部變量前面必須加final修飾,為了延長(zhǎng)局部變量的聲明周期
final Printer2 p = new Printer2();
new Thread() {
public void run() {
while(true) {
p.print1();
}
}
}.start();
new Thread() {
public void run() {
while(true) {
p.print2();
}
}
}.start();
}
}
class Printer2 {
Demo d = new Demo();
//非靜態(tài)的同步方法的鎖對(duì)象是神馬?
//答:非靜態(tài)的同步方法的鎖對(duì)象是this
//靜態(tài)的同步方法的鎖對(duì)象是什么?
//是該類的字節(jié)碼對(duì)象
public static synchronized void print1() {//同步方法只需要在方法上加synchronized關(guān)鍵字即可
System.out.print("黑");
System.out.print("馬");
System.out.print("程");
System.out.print("序");
System.out.print("員");
System.out.print("\r\n");
}
public static void print2() {
synchronized(Printer2.class) {
System.out.print("傳");
System.out.print("智");
System.out.print("播");
System.out.print("客");
System.out.print("\r\n");
}
}
}
死鎖
同步代碼塊的嵌套就容易出現(xiàn)死鎖。所以開發(fā)中盡量避免同步代碼塊的嵌套。
代碼演示:
public class Demo5_DeadLock {
/**
@param args
*/
private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
new Thread() {
public void run() {
while(true) {
synchronized(s1) {
System.out.println(getName() + "...獲取" + s1 + "等待" + s2);
synchronized(s2) {
System.out.println(getName() + "...拿到" + s2 + "開吃");
}
}
}
}
}.start();
new Thread() {
public void run() {
while(true) {
synchronized(s2) {
System.out.println(getName() + "...獲取" + s2 + "等待" + s1);
synchronized(s1) {
System.out.println(getName() + "...拿到" + s1 + "開吃");
}
}
}
}
}.start();
}
}
Vector,StringBuffer,Hashtable這些類之所以說(shuō)是同步的,是因?yàn)樗麄兝锩娴姆椒ㄉ隙技恿藄ynchronized
- Collections.synchroinzedXxx(xxx):可以返回一個(gè)線程安全的集合
- public static <T> Collection<T> synchronizedCollection(Collection<T> c)
- public static <T> Set<T> synchronizedSet(Set<T> s)
- public static <T> List<T> synchronizedList(List<T> list)
- public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
注:Vector是線程安全的,ArrayList是線程不安全的
? StringBuffer是線程安全的,StringBuilder是線程不安全的
? Hashtable是線程安全的,HashMap是線程不安全的
想了解更多學(xué)習(xí)知識(shí),請(qǐng)關(guān)注微信公眾號(hào)“阿Q說(shuō)”,獲取更多學(xué)習(xí)資料吧!你也可以后臺(tái)留言說(shuō)出你的疑惑,阿Q將會(huì)在后期的文章中為你解答。每天學(xué)習(xí)一點(diǎn)點(diǎn),每天進(jìn)步一點(diǎn)點(diǎn)。