java多線程基礎(一)

線程的簡介

幾乎每種操作系統都支持進程的概念。進程就是在某種程度上相互隔離的、獨立運行的程序。
線程化是允許多個活動共存于一個進程中的工具。大多數現代的操作系統都支持線程,而且線程的概念以各種形式已存在了好多年。Java 是第一個在語言本身中顯式地包含線程的主流編程語言,它沒有把線程化看作是底層操作系統的工具。
有時候,線程也稱作輕量級進程。就象進程一樣,線程在程序中是獨立的、并發的執行路徑,每個線程有它自己的堆棧、自己的程序計數器和自己的局部變量。但是,與分隔的進程相比,進程中的線程之間的隔離程度要小。它們共享內存、文件句柄和其它每個進程應有的狀態。
進程可以支持多個線程,它們看似同時執行,但互相之間并不同步。一個進程中的多個線程共享相同的內存地址空間,這就意味著它們可以訪問相同的變量和對象,而且它們從同一堆中分配對象。盡管這讓線程之間共享信息變得更容易,但您必須小心,確保它們不會妨礙同一進程里的其它線程。
Java 線程工具和 API 看似簡單。但是,編寫有效使用線程的復雜程序并不十分容易。因為有多個線程共存在相同的內存空間中并共享相同的變量,所以必須小心,確保線程不會互相干擾。

線程的建立

在java中線程的建立有兩種方式:一種時通過繼承Thread類,一種是實現Runnable接口。下面就這兩種方式的展開討論。

繼承Thread類建立線程

創建一個自定義的線程類MyThread,繼承自Thread,重寫run方法。在run()方法中添加要執行的代碼,代碼如下

public class MyThread extends Thread{
@Override
public void run(){
System.out.println("this is a Thread ,MyThread");
}
}

運行類代碼如下;

public class t1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread myThread = new MyThread();
myThread.start();
System.out.println("run over!~");
}
}

運行結果如圖:

Paste_Image.png

圖2-1 運行結果
這是線程常見的一種寫法,通過繼承Thread類的方法。從本次的運行結果來看 在使用多線程技術時,代碼的運行結果與代碼的執行順序或者代用順序是無關的。補充一點,如果多次調用start()方法,就會出現Exception in thread "main" java.lang.IllegalThreadStateException的異常

實現Runnable接口創建線程

如果想要創建的線程類已經有一個父類了,這個時候就不能通過繼承Thread類了,因為java不支持多繼承,在這個時候就需要通過Runnable接口來創建線程類。
創建一個線程類MyRunnable,實現接口Runnable,重寫run()方法,在run()方法中添加要執行的代碼,代碼如下:

public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("運行中");
}
}

運行類代碼:

public class runnableTest {
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("運行結果");
}
}

運行結果如圖:

Paste_Image.png

圖2-2 運行結果
使用繼承Thread類方式來開發多線程應用程序在設計上是有局限的,因為java是單根繼承,不支持多繼承,所以為了改變這種局限性可以使用Runnable接口方式的=來實現多線程技術。
Thread.java類也實現了Runnable接口。那也就意味這構造函數Thread(Runnable target)不光可以傳入Runnable接口的對象,還可以傳入一個Thread類的對象。這樣就完全可以將一個Thread對象中的run()方法交于其他的線程進行調用。

線程中常見方法

方法名 調用實例 作用 備注
currentThread() Thread.currentTnread().getName() 返回代碼段正在被哪個線程調用的信息 ~
isAlive() mythread.isAlive(); 判斷當前線程是否處于活躍狀態 當線程活躍狀態(即線程已經啟動且尚未終止)返回true,否則返貨false
sleep() Thread.sleep(1000); 指定時間內(以毫秒為單位)當前“正在被執行的線程”休眠 需要捕獲InterruptedException異常
getId() mythread.getId(); 獲取線程的唯一標識 ~
start() mythread.start() 啟動線程 ~
run() mythread.run() 運行線程 ~

ps(表中mythread表示實例化的線程類,即MyThread mythread = new MyThread();)

線程的安全

線程調用的隨機性

在前面就提到過線程的執行順序是與調用的順序是無關的,下面就為展現線程具有的隨機特性,創建一個線程類繼承自Thread,在run()方法中數據變量i的值:

public class MyThread extends Thread{
private int i;
public MyThread(int i){
super();
this.i = i;
}
@Override
public void run() {
System.out.println(i);
}
}

在運行類進行調用

public class MyThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
MyThread t3 = new MyThread(3);
MyThread t4 = new MyThread(4);
MyThread t5 = new MyThread(5);
MyThread t6 = new MyThread(6);
MyThread t7 = new MyThread(7);
MyThread t8 = new MyThread(8);
MyThread t9 = new MyThread(9);
MyThread t10 = new MyThread(10);
MyThread t11 = new MyThread(11);
MyThread t12 = new MyThread(12);
MyThread t13 = new MyThread(13);
MyThread t14 = new MyThread(14);

t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
t8.start();
t9.start();
t10.start();
t11.start();
t12.start();
t13.start();
}
}

程序運行后的記過如圖:

Paste_Image.png

通過這個例子可以很清楚的看到執行start()方法的順序并不代表線程啟動的順序。

實例變量

自定義線程類中的實例變量針對其他線程有共享與不共享之分,這在多線程之間進行交互是一個很重要的技術點。
(1) 不共享數據
不共享數據比較簡單,就是每個線程的數據不進行交互,每個線程中的數據只能在本線程中進行改變,可以通過一個簡答的例子進行說明:
創建線程類:

public class MyThread extends Thread{
private int count =5;
public MyThread(String name){
super();
this.setName(name);//設置線程名字
}

@Override
public void run() {
super.run();
while(count>0){
count--;
System.out.println("由"+this.currentThread().getName()+"計算,count="+count);
}
}

}

運行類的代碼如下,創建三個線程,每個線程都有各自的count變量,自己減少自己的count變量的值,這樣的情況就是變量和不共享。

public class RunTest {
public static void main(String[] args){
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}

運行結果如圖:

不共享數據的運行結果.png

(2)共享數據的情況
共享數據的情況就是多個線程可以訪問同一個變量,下面通過一個實例來看下數據共享的情況:
創建線程類:

public class MyThread1 extends Thread{
private int count =5;
@Override
public void run() {
super.run();
this.count--;
//在這里不能用for語句,因為使用同步之后其他線程就得不到運行的機會了。
System.out.println("由"+this.currentThread().getName()+"計算,count="+count);

}
}

運行類代碼如下:

public class RunTest2 {
public static void main(String[] args){
MyThread1 myThread = new MyThread1();
Thread a = new Thread(myThread,"A");
Thread b = new Thread(myThread,"B");
Thread c = new Thread(myThread,"C");
Thread d = new Thread(myThread,"D");
Thread e = new Thread(myThread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}

運行結果如圖:

圖 共享數據運行結果(1).png
圖 共享數據運行結果(2).png

可以看到,這個程序我們可以得到兩種不同的結果,這是非常致命的,這些不穩定的因素可能就會造成程序間接性的bug。因此我們避免這種情況發生是非常由必要的。下面就這種情況的發生來進行分析。
(1)是我們預想要看到的結果,而從(2)結果可以看出,線程A和線程B同時對count進行了處理,產生了“非線程安全”問題。與我們想要的遞減結果是不同的,
在某些JVM中,i--的操作要分為如下3步,
1)獲取原來i值
2)計算j-1
3)對i進行賦值
在這3個步驟中,如果由多個線程同時訪問,那么一定會線程非線程安全問題。
那么如何避免這種情況呢?最簡單的辦法就是在run方法前面加入synchronized關鍵字,使得多個線程在執行run()方法時。以排隊的方式進行處理。當一個線程調用run錢,先判斷run方法有沒有上鎖,如果上鎖,說明有其他的線程正在調用run方法,必須等其他線程對run方法調用結束才可以執行run方法。這樣也就實現了排隊調用run方法的目的,也就達到了按順序對count變量減1的效果了。更多關于synchronized關鍵子的講解會在后文中講解。總之,在這個例子中我們在run方法前面加入synchronized關鍵字后,就值會出現(1)的結果。

線程的啟動、暫停與停止

線程的啟動

start():他的作用是啟動一個新的線程,新的線程會執行相應的run()方法。start()不能被重復的調用。
run() : run()就和普通的成員方法一樣么可以被重復的調用,單獨的調用run()方法的話,會在當前的線程中執行run(),而不會啟動新的線程!
下面通過兩個例子來區別以下線程啟動的兩種方式:
線程類的代碼testStrartThread.java

public class testStrartThread extends Thread {
public testStrartThread(String usename){
super(usename);
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+" is running");
}
}

測試代碼:demo.java

public class Demo {

public static void main(String[] args) {
Thread testStrartThread = new testStrartThread("test run");
System.out.println(Thread.currentThread().getName()+" call testStratThread run");
testStrartThread.run();

Thread testStrartThread1 = new testStrartThread("test start");
System.out.println(Thread.currentThread().getName()+" call testStratThread run");
testStrartThread1.start();
}
}

下面是運行的結果,

Paste_Image.png

結果說明:
(1)testStrartThread.run()是在主線程main中調用的,該run()方法直接運行在主線程mian上。
(2)testStrartThread1.start()會啟動“testStrartThread”,“線程testStrartThread”啟動之后,會啟動run()方法;此時的run()方法是運行在“線程testStrartThread”上。

線程的暫停

suspend和resume可以有效的實現線程的暫停和回復的效果。可以通過以下的方法進行調用:

Thread myThread = new MyThread();
myThread.start();
myThread.suspend()://暫停線程
myThread.sleep(5000);
myThread.resume();//恢復線程

但是在使用的過程中,你會發現suspend()和resume()都被劃上了刪除線,在這里我們簡單的探討下為什么會被摒棄這兩種方法。
獨占
在使用suspen和resume方法時,如果使用不當,極易造成公共的同步對象的獨占,使得其他線程無法訪問公共同步對象。下面以一個例子來進行說明:
線程類:

public class synchronizedObject {
synchronized public void printString(){
System.out.println(" begin");
if(Thread.currentThread().getName().equals("a")){
System.out.println("a thread is suspend forever!");
Thread.currentThread().suspend();
}
System.out.println(" end");
}
}

測試demo類

public class deom {

public static void main(String[] args) {
try {
final synchronizedObject object = new synchronizedObject();
Thread thread = new Thread() {
@Override
public void run() {
object.printString();
}
};
thread.setName("a");
thread.start();
Thread.sleep(1000);

Thread thread2 = new Thread() {
@Override
public void run() {
System.out.println("thread2 is start but can`t into printString methord ");
object.printString();
}
};
thread2.setName("b");
thread2.start();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

最后的結果如圖:

Paste_Image.png

在線程suspend方法時,如果同實例一樣使用,就會導致線程的阻塞。
不同步
在使用suspend和resum方法時也容易出現因為線程的暫停導致數據不同步的情況。實例代碼如下:

public class myobject {

String username= "1";
String password= "111";

public void setValue(String u,String p){
this.username = u;
if(Thread.currentThread().getName().equals("a")){
System.out.println("stop thead a !");
Thread.currentThread().suspend();
}
this.password = p;
}

public void printString(){
System.out.println("username :"+this.username+" password: "+this.password);
}
}

測試類:

public class run {

public static void main(String[] args) {
try {
final myobject my = new myobject();
Thread thread1 = new Thread() {
public void run() {
my.setValue("a", "aa");
}
};
thread1.setName("a");
thread1.start();

Thread.sleep(500);

Thread thread2 = new Thread() {
public void run() {
my.printString();
}
};
thread2.start();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

}

運行結果如圖:

Paste_Image.png

通過這個簡單的例子,我們可以看出來在使用suspend方法時還是有很多的坑的,需要我們仔細去分辨,所以摒棄了suspend方法。
yield方法
yiled()方法的作用是放棄當前的cpu資源,將它讓給其他的任務去占用cpu執行時間。但是放棄的時間不確定,有可能剛剛放棄,馬上又獲得了cup時間片。調用示例:

public class MyThread extends Thread{
public void run(){
for(int i=0;i<10;i++){
system.out.printf("i= "+i+" current");
Thread.yield();//放棄當前的時間片
}
}
}

停止線程

停止線程四在多線程開發過程中很重要的技術點,掌握次技術可以對線程的停止進行有效的處理。停止線程在java語言中并不像break語句那么簡單干脆,需要一些技巧性的處理。
大多數停止線程的操作使用Thread.interrupt()方法,盡管方法名稱是“停止,中止”的意思,但是這個方法不會終止一個正在運行的線程,這需要加入一個判斷才能完成線程的停止。關于此知識點后面會有相應的介紹。
在java中有以下三種方法可以終止正在進行的線程:

  • 1.使用退出標志,使線程正常退出,也就是當run方法執行完成后線程自動終止。
  • 2.使用stop方法強行終止線程,但是不推薦使用這種方法,因為使用他們會產生不可預料的結果。
  • 3.使用inerrupt方法終止線程。

在介紹如何停止線程的知識點之前,先來看一下如何判斷線程的狀態是不是停止的。在java的JDK中,Thread.java類里提供了兩種方法。
1) this.interrupted():測試當前線程是否已經中斷。執行后具有將狀態標志清除為false的功能。官方文檔對interrupted方法的解釋:
測試當前線程是否已經終端。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次返回false(在第一次第啊用已清除了其中斷狀態之后,且第二次第啊用檢驗完中斷狀態錢,當前線程再次中斷的情況除外)。
2)this.isInterrupted():測試線程Thread對象是否已經是終端狀態,但不清除狀態標志。
利用異常退出線程
實例代碼如下:
myThread代碼如下:

public class MyThread extends Thread {

public void run() {
super.run();
try {
for (int i = 0; i < 20000; i++) {
if (this.interrupted()) {
System.out.println("it had stop,will exit!");
throw new InterruptedException();//拋出一個異常,直接退出線程
}
System.out.println("i= " + i);
}
System.out.println("this thread is`t stop");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

測試類的代碼如下:

public class Run {
public static void main(String[] args) {
try {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(20);
myThread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}

運行結果如下:

Paste_Image.png

如果只是使用interrupted方法來停止線程,本此測試程序將無法停止MyThread的線程(證明就是即使停止線程,for循環的System.out.println("this thread is`t stop");語句將會被執行),可以自己親自嘗試一下。
使用return停止線程
將方法interrupted與return相結合也能實現停止線程的效果,使用方法同異常法大同小異,只需要將前面例子中的MyThread線程類改成下面的代碼:

public class MyThread extends Thread {

public void run() {
super.run();
for (int i = 0; i < 20000; i++) {
if (this.interrupted()) {
System.out.println("it had stop,will exit!");
return;
}
System.out.println("i= " + i);
}
System.out.println("this thread is`t stop");
}
}

運行類與上例一樣。
運行效果如圖:

Paste_Image.png

可以看出來,結合return來使用也可以達到停止線程的效果。但是還是建議用拋異常的方法來實現線程的停止,因為在catch塊中還可以將異常向上拋出,使線程停止的方法可以得到傳播。
stop暴力停止方法
使用stop方法停止線程非常簡單,形如:"this.stop();"即可停止線程,在這里不多多講它的使用,著重講的是該方法的副作用和使用時要注意的事項 。
調用stop()方法時會拋出java.lang.ThreadDeath異常,但在通常情況下,此異常不需要顯示的捕獲。方法stop已經被作廢,因為如果使用強制讓線程停止則可能使一些清理性工作得不到完成。另外一種情況就是對鎖定的對象進行了“解鎖”,導致數據得不到同步的處理,出現數據不一致的問題。

最后注意幾點:
在sleep狀態下停止線程,會進入到catch語句,并且清除停止狀態位,使之變成false。
若在停止線程后在進入sleep狀態,則在會運行完線程后進入到中斷的catch語句中。

線程的優先級

在操作系統中,線程可以劃分優先級,優先級高的線程可以得到的cpu資源較多,也就是cpu優先執行優先級較高的線程對象中任務。
設置線程優先級有助于“線程規劃器”確定在下一次選擇那一個線程來優先執行。設置線程優先級使用:setPriority()方法。在java的JDK中,線程的優先級劃分為1-10這10個等級,如果不在此范圍內,則會拋出異常。
設置線程優先級:

this.setPriority(6);

下面對線程優先級做幾點說明:

繼承性

在java中,線程的優先級具有繼承性,比如a線程啟動b線程,則b線程的優先級與a是一樣的。在次就不局里說明了。

規則性

高優先級的線程總是大部分先執行完,但不代表高優先級的線程全部先執行完。另外,當線程優先級差距很大的時候,誰先執行完和代碼的執行順序無關。

隨機性

優先級高的線程不一定每一次都先執行完。不要把線程的優先級與運行結果的順序作為衡量的標準,優先級較高的線程并不一定每一都先執行完run方法中的任務,也就是說,線程的優先級與打印的順序無關,不要將這兩者相關聯,他們的關系具有不確定性和隨機性。(在優先級別差不多的情況下體現的比較明顯)。

守護線程

在java線程中,有兩種線程,一種是用戶線程,一種是守護線程。
用戶線程就是前面介紹的基本線程,守護線程是一種特殊的線程,他的特性是有“陪伴”的含義。當進程中不存在與非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有存在的必要了,自動銷毀。下面通過一個demo來理解守護線程:
創建一個線程類MyThread

public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("私有變量i=" + (i));
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

運行類:

public class Runtest {

public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);//通過這個語句設置為守護線程
thread.start();
Thread.sleep(3000);
System.out.println("我離開thread對象就不在打印,也就是停止了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

運行結果如圖所示:

守護線程運行結果.png

這里,MyThread就是守護線程,守護我們的主線程,在RunTest中讓主線程休眠了3s,所以MyThread線程執行了3s,1s打印一次,所以出現上面的結果。當主線程休眠結束,MyThread的守護線程也自動結束了,所以它也不在打印任何值了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容