11、多線程


十一、多線程

1、系統結構圖(xmind)

2、tips

——1.多線程概述

JVM啓動時啓動了多條線程,至少有兩個線程可以分析的出來:

1.執行main函數的線程,該線程的任務代碼都定義在main函數中

2.負責垃圾回收的線程。

System類的gc方法告訴垃圾回收器調用finalize方法,但不一定立即執行。


——2.繼承Thread類

       創建線程的目的就是爲了開啓一條執行路徑,去運行指定的代碼和其他代碼實現同時運行,而運行的指定代碼就是這個執行路徑的任務,jvm創建的主線程的任務都定義在了主函數中,而自定義的線程,它的任務在哪兒呢?Thread類用於描述線程,線程是需要任務的,所以Thread類也有對任務的描述,這個任務就是通過Thread類中的run方法來體現,也就是說,run方法就是封裝自定義線程運行任務的函數,run方法中定義的就是線程要運行的任務代碼。開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並覆寫run方法,將運行的代碼定義在run方法中即可。


多線程栗子:

class Demo extends Thread
{
	private String name;
	Demo(String name)
	{
		this.name = name ;
	}
	//複寫Thread類的run方法
	public void run()
	{
		//Thread.currentThread().getName()當前線程名字
		for(int x = 0;x < 10;x++)
		{
			System.out.println(name+"...x="+x+"....Threadname:"+Thread.currentThread().getName());
		}
	}
}
class ThreadDemo4
{
	public static void main(String[] args)
	{
		Demo d1 = new Demo("毛1");
		Demo d2 = new Demo("毛2");
		//開啓線程,調用run方法
		d1.start();
		d2.start();
		for(int x=0;x<10;x++)
		{
			System.out.println("x:"+x+"...over....."+"Threadname:"+Thread.currentThread().getName());
		}
	}
}

運行結果:

1.可以通過Thread的getName方法獲取線程的名稱,名稱格式:Thread-編號(從0開始)。
2.Thread在創建的時候,該Thread就已經命名了。

——3.實現Runnable接口

慄:

//準備擴展Demo類的功能,讓其中的內容可以作爲線程的任務執行
//通過接口的形式完成
class Demo implements Runnable
{
	//複寫run方法
	public void run()
	{
		show();
	}
	public void show()
	{
		for(int x=0;x<10;x++)
		{
			System.out.println(Thread.currentThread().getName()+"...."+x);
		}
	}
}
class ThreadDemo1
{
	public static void main(String[] args)
	{
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.start();
		t2.start();
	}
}


運行結果:

      


——4.線程安全問題

慄:

/*
需求:模擬4個線程賣100張票。
*/
class Ticket implements Runnable 
{
	private int num = 100;
	Object obj = new Object();
	public void run()
	{
		while (true)
		{
			//同步鎖,當有線程進入時,將會鎖住,在當前線程爲運行完前不允許進入
			synchronized(obj)
			{
				if (num > 0)
				{
					System.out.println(Thread.currentThread().getName()+"..."+num--);
				}
			}
		}
	}
}
class TicketDemo2
{
	public static void main(String[] args)
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}


運行結果:

           

           

原因分析:從運行結果可以得知,安全問題已經解決,原因在於Object對象相當於是一把鎖,只有搶到鎖的線程,才能進入同步代碼塊向下執行。當num=1時,Cpu切換到某個線程後,其他線程將無法通過同步代碼塊繼而進行if判斷語句,只有等待前一個線程執行完num--操作,並且跳出同步代碼塊後,才能搶到鎖,其他線程即使搶到鎖,然而,此時num已經是0,也就無法通過if語句判斷,從而無法再執行num--操作,也就不會出現0、-1、-2的情況了。


——5.同步函數和同步代碼塊的區別

1.同步代碼塊的鎖是任意的對象

2.同步函數的鎖是固定的this。

建議使用同步代碼塊

       同步函數的鎖是固定的this,同步代碼塊的鎖是任務的對象,那麼如果同步函數和同步代碼塊都使用this作爲鎖,就可以實現同步。

       靜態的同步函數使用的鎖是該函數鎖屬字節碼文件對象,可以用getClass方法獲取,也可以用當前類名.class表示。

慄:

class Ticket implements Runnable 
{
	private int num = 100;
	Object obj = new Object();
	boolean flag = true;
	public void run()
	{
		if(flag)
		{
			while(true)
			{
				//使用this作爲鎖,而線程sleep需要拋出異常所以使用try/catch捕捉
				synchronized(this.getClass())//Ticket.Class()
				{
					if(num>0)
					{
						try
						{
							Thread.sleep(10);
						}
						catch (InterruptedException e)
						{
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()+"...."+num--);
					}
				}
			}
		}
		else
			while(true)
				show();
	}
	//當flag爲false時執行同步函數
	public synchronized void show()
	{
		if(num > 0)
		{
			try
			{
				Thread.sleep(10);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"...."+num--);
		}
	}
}
class SynFunctionLockDemo2
{
	public static void main(String[] args)
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		/*
		需要sleep10毫秒的原因是因爲可能線程t1尚未真正啓動,flag就被設置爲false,那麼當ti執行
		時,就會按照flag爲false的情況執行,線程t2也按照flag爲false的情況執行.
		*/
		try
		{
			Thread.sleep(10);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		t.flag = false;
		t2.start();
	}
}


運行結果:


——6.多線程下的單例模式

餓漢式不存在安全問題,因爲不存在多個線程共同操作數據的情況

懶漢式存在安全問題,能夠使用同步函數解決:

class Single
{
	public static Single s =null;
	public void Single(){}
	public static Single getInstance()
	{
		if(s == null)
		{
			Synchronized(this.getClass())//Single.class
			{
				if(s == null)
					s = new Single();
			}
		}
		return s;
	}
}

如果使用同步函數,效率較低,因爲每次都需要判斷,而上部代碼的方式不會有這樣的問題,當任何一個線程執行到第一個if判斷語句時,Single對象已經創建,則直接能夠獲取,不用判斷是否能夠獲取鎖,相對於使用同步函數的方法就提升了效率。


——7.死鎖

當同步中嵌套同步時就有可能出現死鎖的現象。

慄:

/* 
寫一個死鎖程序 
*/  
  
//定義一個類來實現Runnable,並複寫run方法  
class LockTest implements Runnable  
{  
    private boolean flag;  
    LockTest(boolean flag)  
    {  
        this.flag=flag;  
    }  
    public void run()  
    {  
        if(flag)  
        {  
            while(true)  
            {  
                synchronized(LockClass.locka)//a鎖  
                {  
                    System.out.println(Thread.currentThread().getName()+"------if_locka");  
  
                    synchronized(LockClass.lockb)//b鎖  
                    {  
                    System.out.println(Thread.currentThread().getName()+"------if_lockb");  
                    }  
                }  
            }  
        }  
        else  
        {  
            while(true)  
            {  
                synchronized(LockClass.lockb)//b鎖  
                {  
                  System.out.println(Thread.currentThread().getName()+"------else_lockb");  
  
                    synchronized(LockClass.locka)//a鎖  
                    {  
                   System.out.println(Thread.currentThread().getName()+"------else_locka");  
                    }  
                }  
            }  
        }  
    }  
}  
  
//定義兩個鎖  
class LockClass  
{  
    static Object locka = new Object();  
    static Object lockb = new Object();  
}  
  
class DeadLock  
{  
   public static void main(String[] args)  
   {  
       //創建2個進程,並啓動  
       new Thread(new LockTest(true)).start();  
       new Thread(new LockTest(false)).start();  
   }  
} 


運行結果:


——8.線程中通信

等待/喚醒機制設計的方法

1.這些方法都必須定義在同步中,因爲這些方法是用於操作線程狀態的方法。

2.必須要明確到底操作的是哪個鎖上的線程!

wait和sleep區別?

1)wait可以指定時間也可以不指定。sleep必須指定時間。

2)在同步中時,對CPU的執行權和鎖的處理不同。

        wait:釋放執行權,釋放鎖。

        sleep:釋放執行權,不釋放鎖。

        爲什麼操作線程的方法wait、notify、notifyAll定義在了object類中,因爲這些方法是監視器的方法,監視器其實就是鎖。鎖可以是任意的對象,任意的對象調用的方式一定在object類中。


慄:

/*
	有一個資源
一個線程往裏存東西,如果裏邊沒有的話
一個線程往裏取東西,如果裏面有得話
*/

//資源
class Resource
{
	private String name;
	private String sex;
	private boolean flag=false;
	
	public synchronized void setInput(String name,String sex)
	{
		if(flag)
		{
			try{wait();}catch(Exception e){}//如果有資源時,等待資源取出
		}
		this.name=name;
		this.sex=sex;

		flag=true;//表示有資源
		notify();//喚醒等待
	}

	public synchronized void getOutput()
	{		
		if(!flag)
		{
			try{wait();}catch(Exception e){}//如果木有資源,等待存入資源
		}
		System.out.println("name="+name+"---sex="+sex);//這裏用打印表示取出
				
		flag=false;//資源已取出
		notify();//喚醒等待
	}
}


//存線程
class Input implements Runnable
{
	private Resource r;
	Input(Resource r)
	{
		this.r=r;
	}
	public void run()//複寫run方法
	{
		int x=0;
		while(true)
		{
			if(x==0)//交替打印張三和王羲之
			{
				r.setInput("謝",".....man");
			}
			else
			{
				r.setInput("毛","..woman");
			}
			x=(x+1)%2;//控制交替打印
		}
	}
}

//取線程
class Output implements Runnable
{
	private Resource r;
	Output(Resource r)
	{
		this.r=r;
	}
	public void run()//複寫run方法
	{
		while(true)
		{
			r.getOutput();
		}
	}
}
<strong>

</strong>
class ResourceDemo6
{
	public static void main(String[] args) 
	{
		Resource r = new Resource();//表示操作的是同一個資源

		new Thread(new Input(r)).start();//開啓存線程

		new Thread(new Output(r)).start();//開啓取線程
	}
}<strong>
</strong>

運行結果:
       

幾個小問題:
1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?
       a,這些方法存在與同步中。
       b,使用這些方法時必須要標識所屬的同步的鎖。同一個鎖上wait的線程,只可以被同一個鎖上的notify喚醒。
       c,鎖可以是任意對象,所以任意對象調用的方法一定定義Object類中。
2)wait(),sleep()有什麼區別?
       wait():釋放cpu執行權,釋放鎖。
       sleep():釋放cpu執行權,不釋放鎖。
3)爲甚麼要定義notifyAll?
因爲在需要喚醒對方線程時。如果只用notify,容易出現只喚醒本方線程的情況。導致程序中的所以線程都等待。

——9.JDK1.5新特性

慄:
/*
生產者生產商品,供消費者使用
有兩個或者多個生產者,生產一次就等待消費一次
有兩個或者多個消費者,等待生產者生產一次就消費掉

*/

import java.util.concurrent.locks.*;

class Resource 
{	
	private String name;
	private int count=1;
	private boolean flag = false;
	
	//多態
	private Lock lock=new ReentrantLock();

	//創建兩Condition對象,分別來控制等待或喚醒本方和對方線程
	Condition condition_pro=lock.newCondition();
	Condition condition_con=lock.newCondition();

	//p1、p2共享此方法
	public void setProducer(String name)throws InterruptedException
	{
		lock.lock();//鎖
		try
		{
			while(flag)//重複判斷標識,確認是否生產
				condition_pro.await();//本方等待

			this.name=name+"......"+count++;//生產
			System.out.println(Thread.currentThread().getName()+"...生產..."+this.name);//打印生產
			flag=true;//控制生產\消費標識
			condition_con.signal();//喚醒對方
		}
		finally
		{
			lock.unlock();//解鎖,這個動作一定執行
		}
		
	}

	//c1、c2共享此方法
	public void getConsumer()throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)//重複判斷標識,確認是否可以消費
				condition_con.await();

			System.out.println(Thread.currentThread().getName()+".消費."+this.name);//打印消費
			flag=false;//控制生產\消費標識
			condition_pro.signal();
		}
		finally
		{
			lock.unlock();
		}

	}
}

//生產者線程
class Producer implements Runnable 
{
	private Resource res;
	Producer(Resource res)
	{
		this.res=res;
	}
	//複寫run方法
	public void run()
	{
		while(true)
		{
			try
			{
				res.setProducer("商品");
			}
			catch (InterruptedException e)
			{
			}
		}
	}
}

//消費者線程
class Consumer implements Runnable
{
	private Resource res;
	Consumer(Resource res)
	{
		this.res=res;
	}
	//複寫run
	public void run()
	{
		while(true)
		{
			try
			{
				res.getConsumer();
			}
			catch (InterruptedException e)
			{
			}
		}
	}

}

class  ProducerConsumer
{
	public static void main(String[] args) 
	{
		Resource res=new Resource();

		new Thread(new Producer(res)).start();//第一個生產線程 p1
		new Thread(new Consumer(res)).start();//第一個消費線程 c1

		new Thread(new Producer(res)).start();//第二個生產線程 p2
		new Thread(new Consumer(res)).start();//第二個消費線程 c2
	}
}


運行結果:


——10.停止線程
1.開啓多線程運行,運行代碼通常是循環結構。只要控制住循環,就可以讓run方法結束,也就是線程結束。
     如:run方法中有如下代碼,設置一個flag標記。

public  void run()
{
	while(flag)
	{	
		System.out.println(Thread.currentThread().getName()+"....run");
	}
}

那麼只要在主函數或者其他線程中,在該線程執行一段時間後,將標記flag賦值false,該run方法就會結束,線程也就停止了。
2、上面的1方法可以解決一般情況,但是有一種特殊情況:就是當線程處於凍結狀態。就不會讀取到標記。那麼線程就不會結束。
        當沒有指定的方式讓凍結的線程恢復到運行狀態時,這時需要對凍結進行清除。強制讓線程恢復到運行狀態中來。這樣就可以操作標記讓線程結束。Thread類提供該方法interrupt();
如:
class StopThread implements Runnable
{
	private boolean flag =true;
	public  void run()
	{
		while(flag)
		{
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	public void changeFlag()
	{
		flag = false;
	}
}

class  StopThreadDemo
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);	
        t1.start();
        t2.start();	

		int num = 0;
		while(true)
		{
			if(num++ == 60)
			{
				t1.interrupt();//清除凍結狀態
				t2.interrupt();
				st.changeFlag();//改變循環標記
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}

運行結果:
      

——11.擴展
1、join方法
        當A線程執行到了b線程的.join()方法時,A線程就會等待,等B線程都執行完,A線程纔會執行。(此時B和其他線程交替運行。)join可以用來臨時加入線程執行。
2、setPriority()方法用來設置優先級
        MAX_PRIORITY 最高優先級10
        MIN_PRIORITY   最低優先級1
        NORM_PRIORITY 分配給線程的默認優先級
3、yield()方法可以暫停當前線程,讓其他線程執行。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章