線程的了解
1、同一個應用中,多個任務同時進行。就像editplus編輯工具,打開一個檔案視窗就是一個線程。
2、線程可以有多個,但cpu每時每刻隻做一件事(多核除外)。由于cpu處理速度很快,我們就感覺是同時進行的。是以宏觀上,線程是并發進行的;從微觀角度看,線程是異步執行的。
3、使用線程的目的是最大限度的利用cpu資源。想想當你在editplus中按下"ctrl + shift +S"儲存全部檔案的時候,如果要儲存的檔案比較多,沒有多線程的話,前面的檔案沒有完成操作的話,後面的操作是執行不了的~!
建立線程
實作多線程,有兩種手段:
一:繼承Thread類(啟動線程方法:new MyThread().start();)
步驟:
1,定義類繼承Thread。
2,複寫Thread類中的run方法。
目的:将自定義代碼存儲在run方法。讓線程運作。
3,調用線程的start方法,
該方法兩個作用:啟動線程,調用run方法。
線程中,start和run的差別
start():是開啟線程并調用run方法
run():存放的是線程執行的代碼,如果單純的調用run方法,隻是普通的建立對象調用方法,而并沒有開啟線程
class 類名 extends Thread{
@Override
public void run() {
//code
}
}
class Show extends Thread
{
public void run()
{
for (int i =0;i<5 ;i++ )
{
System.out.println(name +"_" + i);
}
}
public Show(){}
public Show(String name)
{
this.name = name;
}
private String name;
public static void main(String[] args)
{
new Show("csdn").run();
new Show("黑馬").run();
}
}
【運作結果】:
發現這些都是順序執行的,說明調用run()方法不對,應該調用的是start()方法。
把上面的主函數修改為如下:
public static void main(String[] args)
{
new Show("csdn").start();
new Show("黑馬").start();
}
在指令行執行: javac Show.java java Show,輸出的可能的結果如下:
因為需要用到CPU的資源,多個線程都擷取cpu的執行權,cpu執行到誰,誰就運作。是以每次的運作結果基本是都不一樣的;
這也是多線程的一個特性:随機性。誰搶到誰執行,至于執行多長,cpu說了算。
那麼:為什麼我們不能直接調用run()方法呢?
我的了解是:線程的運作需要本地作業系統的支援。
如果你檢視start的源代碼的時候,會發現:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
在start0()方法中jvm調用run()方法,這個這個方法用了native關鍵字,native表示調用本地作業系統的函數,多線程的實作需要本地作業系統的支援。
二.實作Runable接口,(啟動線程方法:new Thread(new MyRunnable()).start();)
class 類名 implements Runnable {
@Override
public void run() {
//code
}
}
class Show implements Runnable
{
public Show(){}
public Show(String name)
{
this.name = name;
}
@Override
public void run()
{
for (int i =0;i<5 ;i++ )
{
System.out.println(name +"_" + i);
}
}
private String name;
public static void main(String[] args)
{
new Thread(new Show("csdn")).start();
new Thread(new Show("黑馬")).start();
}
}
【可能的運作結果】:
關于選擇繼承Thread還是實作Runnable接口?
其實Thread也是實作Runnable接口的:
public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
其實 Thread 中的 run 方法調用的是 Runnable 接口的 run 方法。不知道大家發現沒有, Thread 和 Runnable 都實作了 run 方法,這種操作模式其實就是代理模式。
Thread和Runnable的差別:
如果一個類繼承Thread,則不能資源共享(有可能是操作的實體不是唯一的);但是如果實作了Runable接口的話,則可以實作資源共享。
class Show extends Thread
{
@Override
public void run()
{
for (int i = 0; i < 5 ; i++ )
{
if (count > 0)
{
System.out.println(Thread.currentThread().getName()+": count=" + count--);
}
}
}
private int count = 5;
public static void main(String[] args)
{
new Show().start();//new 出來多個實體
new Show().start();
}
}
【運作結果】:
public static void main(String[] args)
{
Show s = new Show();
s.start();
s.start();
}
我們再以實作Runnable接口的方式修改上面的程式:
class Show implements Runnable
{
private int count = 10;//假設有10張票
@Override
public void run()
{
for (int i = 0; i < 5 ; i++ )
{
if (this.count > 0)
{
System.out.println(Thread.currentThread().getName()+"正在賣票" + this.count--);
}
}
}
public static void main(String[] args)
{
Show s = new Show(); //注意必須保證隻對1個實體s操作
new Thread(s,"視窗1").start();
new Thread(s,"視窗2").start();
new Thread(s,"視窗3").start();
}
}
【運作結果】:
總結:
實作Runnable接口比繼承Thread類所具有的優勢:
1):适合多個相同的程式代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程式的健壯性,代碼可以被多個線程共享,代碼和資料獨立。
是以,還是以 實作接口的方式來建立好些。
在java中所有的線程都是同時啟動的,至于什麼時候,哪個先執行,完全看誰先得到CPU的資源;每次程式運作至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java指令執行一個類的時候,實際上都會啟動一個JVM,每一個jVM實際上就是在作業系統中啟動了一個程序。
設定線程優先級(主線程的優先級是5,不要誤以為優先級越高就先執行。誰先執行還是取決于誰先去的CPU的資源)
<span style="font-size:14px;">Thread t = new Thread(myRunnable);
t.setPriority(Thread.MAX_PRIORITY);//一共10個等級,Thread.MAX_PRIORITY表示最進階10
t.start();</span>
判斷線程是否啟動
class Show implements Runnable
{
public void run()
{
for (int i = 0; i < 5 ; i++ )
{
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args)
{
Show s = new Show();
Thread t = new Thread(s);
System.out.println("線程啟動前:" + t.isAlive());
t.start();
System.out.println("線程啟動後:" + t.isAlive());
}
}
【運作結果】:
主線程也有可能在子線程結束之前結束。并且子線程不受影響,不會因為主線程的結束而結束。
join,sleep,yield的用法與差別
join方法:假如你在A線程中調用了B線程的join方法B.join();,這時B線程繼續運作,A線程停止(進入阻塞狀态)。等B運作完畢A再繼續運作。
sleep方法:線程中調用sleep方法後,本線程停止(進入阻塞狀态),運作權交給其他線程。
yield方法:線程中調用yield方法後本線程并不停止,運作權由本線程和優先級不低于本線程的線程來搶。(不一定優先級高的能先搶到,隻是優先級高的搶到的時間長)
背景線程
<span style="font-size:14px;">Thread t = new Thread(new Show());
t.setDaemon(true);
t.start();</span>
在java程式中,隻要前台有一個線程在運作,整個java程式程序不會消失,是以此時可以設定一個背景線程,這樣即使java程序消失了,此背景線程依然能夠繼續運作。
結束線程(修改标示符flag為false來終止線程的運作)
<span style="font-size:14px;">class Show implements Runnable
{
private boolean flag = true;
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+" is living");
}
}
public void shutDown()
{
this.flag = false;
}
public static void main(String[] args)
{
Show s = new Show();
Thread t = new Thread(s);
t.start();
try
{
Thread.sleep(2);
}
catch (Exception e)
{
e.printStackTrace();
}
s.shutDown();
}
}</span><span style="color: rgb(128, 128, 128); font-size: 15px;">
</span>
線程同步synchronized
synchronized可以修飾方法,或者方法内部的代碼塊。被synchronized修飾的代碼塊表示:一個線程在操作該資源時,不允許其他線程操作該資源。
【問題引出】:比如說對于賣票系統,有下面的代碼:
class Show implements Runnable
{
private int count =5;//5張票要賣
public void run()
{
for (int i = 0; i < 10 ; i++ )
{
if (count > 0)
{
try
{
Thread.sleep(200);
}
catch (Exception e)
{
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+","+count--);
}
}
public static void main(String[] args)
{
Show s = new Show();
new Thread(s,"A").start();
new Thread(s,"B").start();
new Thread(s,"C").start();
}
}
【運作結果】:
這裡出現了負數,顯然這個是錯的。,應該票數不能為負值。
如果想解決這種問題,就需要使用同步。
所謂同步就是在統一時間段中隻有有一個線程運作,其他的線程必須等到這個線程結束之後才能繼續執行。
【使用線程同步解決問題】
采用同步的話,可以使用同步代碼塊和同步方法兩種來完成。
同步代碼塊:
文法格式:
synchronized(同步對象){
//需要同步的代碼
}
但是一般都把目前對象this作為同步對象。
class Show implements Runnable
{
private int count =5;
public void run()
{
for (int i = 0; i < 20 ; i++ )
{
synchronized(this)
{
if (count > 0)
{
try
{
//Thread.currentThread().yield();
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+","+count--);
}
}
}
}
public static void main(String[] args)
{
Show s = new Show();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t3.start();
t1.start();
t2.start();
}
}
【運作結果】:
也可以采用同步方法:
文法格式為synchronized 方法傳回類型方法名(參數清單){
// 其他代碼
}
class Show implements Runnable
{
private int count =5;
public void run()
{
for (int i = 0; i < 20 ; i++ )
{
sale();
}
}
public synchronized void sale()
{
if (count > 0)
{
try
{
//Thread.currentThread().yield();
Thread.sleep(300);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+","+count--);
}
}
public static void main(String[] args)
{
Show s = new Show();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
t3.start();
t1.start();
t2.start();
}
}
同步的前提:
1,必須要有兩個或者兩個以上的線程。
2,必須是多個線程使用同一個鎖。
必須保證同步中隻能有一個線程在運作。
好處:解決了多線程的安全問題。
弊端:多個線程都需要判斷鎖,較為消耗資源,
wait、notify、notifyAll的用法
wait方法:目前線程轉入阻塞狀态,讓出cpu的控制權,解除鎖定。
notify方法:喚醒因為wait()進入阻塞狀态的其中一個線程。
notifyAll方法: 喚醒因為wait()進入阻塞狀态的所有線程。
這三個方法都必須用synchronized塊來包裝,而且必須是同一把鎖,不然會抛出java.lang.IllegalMonitorStateException異常。
當多個線程共享一個資源的時候需要進行同步,但是過多的同步可能導緻死鎖,同步中嵌套同步容易引發死鎖。
此處列舉經典的【生産者和消費者問題】:
class Info {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String name = "lfz";
private int age = 20;
}
/**
* 生産者
*/
class Producer implements Runnable{
private Info info=null;
Producer(Info info){
this.info=info;
}
public void run(){
boolean flag=false;
for(int i=0;i<10;++i){
while (true)
{
if(flag){
this.info.setName("adanac");
this.info.setAge(20);
flag=false;
}else{
this.info.setName("jean");
this.info.setAge(30);
flag=true;
}
}
}
}
}
/**
* 消費者類
* */
class Consumer implements Runnable{
private Info info=null;
public Consumer(Info info){
this.info=info;
}
public void run(){
for(int i=0;i<10;++i){
System.out.println(this.info.getName()+"<---->"+this.info.getAge());
}
}
}
class Show
{
public static void main(String[] args)
{
Info info=new Info();
Producer pro=new Producer(info);
Consumer con=new Consumer(info);
new Thread(pro).start();
new Thread(con).start();
}
}
從結果中看到,名字和年齡并沒有對應。
那麼如何解決呢?
1)加入同步
2)加入等待和喚醒
先來看看加入同步會是如何:
class Info {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void set(String name, int age){
this.name=name;
this.age=age;
}
public synchronized void get(){
System.out.println(this.getName()+"<===>"+this.getAge());
}
private String name = "lfz";
private int age = 20;
}
/**
* 生産者
*/
class Producer implements Runnable{
private Info info=null;
Producer(Info info){
this.info=info;
}
public void run(){
boolean flag=false;
for(int i=0;i<10;++i){
while (true)
{
if(flag){
this.info.set("adanac",10);
flag=false;
}else{
this.info.set("lfz",20);
flag=true;
}
}
}
}
}
/**
* 消費者類
* */
class Consumer implements Runnable{
private Info info=null;
public Consumer(Info info){
this.info=info;
}
public void run(){
for(int i=0;i<10;++i){
try{
Thread.sleep(100);
}catch (Exception e) {
e.printStackTrace();
}
this.info.get();
}
}
}
class Show
{
public static void main(String[] args)
{
Info info=new Info();
Producer pro=new Producer(info);
Consumer con=new Consumer(info);
new Thread(pro).start();
new Thread(con).start();
}
}
運作結果來看,錯亂的問題解決了,現在是adanac對應20,jean對于30。
但是還是出現了重複讀取的問題,也肯定有重複覆寫的問題。
如果想解決這個問題,就需要使用Object類幫忙了,我們可以使用其中的等待和喚醒操作。
要完成上面的功能,我們隻需要修改 Info 類饑渴,在其中加上标志位,并且通過判斷标志位完成等待和喚醒的操作,代碼如下:
class Info {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public synchronized void set(String name, int age){
if (!flag)
{
try
{
super.wait();
}
catch (Exception e)
{
e.printStackTrace();
}
}
this.name=name;
this.age=age;
flag = false;
super.notify();
}
public synchronized void get(){
if (flag)
{
try
{
super.wait();
}
catch (Exception e)
{
e.printStackTrace();
}
}
System.out.println(this.getName()+"<===>"+this.getAge());
flag = true;
super.notify();
}
private String name = "xiaoli";
private int age = 22;
private boolean flag = false;
}
/**
* 生産者
*/
class Producer implements Runnable{
private Info info=null;
Producer(Info info){
this.info=info;
}
public void run(){
boolean flag=false;
for(int i=0;i<10;++i){
while (true)
{
if(flag){
this.info.set("adanac",10);
flag=false;
}else{
this.info.set("lfz",20);
flag=true;
}
}
}
}
}
/**
* 消費者類
* */
class Consumer implements Runnable{
private Info info=null;
public Consumer(Info info){
this.info=info;
}
public void run(){
for(int i=0;i<10;++i){
this.info.get();
}
}
}
class Show
{
public static void main(String[] args)
{
Info info=new Info();
Producer pro=new Producer(info);
Consumer con=new Consumer(info);
new Thread(pro).start();
new Thread(con).start();
}
}
【運作結果】: