Java 多線程同步和多線程安全

一,多線程同步

1.1,多線程同步定義:

一次只有一個線程可以讀寫共享變量。當有一個線程正在訪問共享變量時,其他線程應該等到第一個線程完成之後再訪問。並且多個線程不會干擾。(多個線程同時操作一個對象,在各種不同情況下,都不會造成不同的後果。)

注意區分這幾個概念:線程 多線程 多線程併發 多線程安全 多線程同步

併發(concurrency)簡單來說,就是cpu在同一時刻要執行多個任務。Java併發則由多線程實現的;

1.2,什麼情況下需要同步

       當多線程併發, 有多段代碼同時執行時, 我們希望某一段代碼執行的過程中CPU不要切換到其他線程工作. 這時就需要同步.

       多線程同步之間的關係:沒多線程環境就不需要同步,因爲單線程本來就是同步執行的;有多線程環境也不一定需要同步。 

1.3,多線程同步的幾種方式

1.3.1,Synchronized關鍵字

  • 使用synchronized必須有一些條件:

        1.必須要有兩個或者兩個以上的線程需要發生同步。

        2.多個線程想同步,必須使用同一把鎖

        3.保證只有一個線程進行執行

  • synchronized原理:

        1.首先有一個線程已經拿到了鎖,其他線程已經有cup執行權,一直排隊,等待釋放鎖。

        2.鎖是在什麼時候釋放?代碼執行完畢或者程序拋出異常都會被釋放掉。

        3.鎖已經被釋放掉的話,其他線程開始進行搶鎖(資源競爭),誰搶到誰進入同步中去,其他線程繼續等待。

  • 弊端:效率非常低,多個線程需要判斷鎖,比較消耗資源,搶鎖的資源。

synchronized使用有實例方法中的同步代碼塊、靜態方法中的同步代碼塊、實例同步方法、靜態同步方法;

  • 實例方法中的同步代碼塊

       案例:

public class SynchronizedDemo2 {
	
	public static void main(String[] args) {
		final OutPrint3 op = new OutPrint3();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while(true){
					op.out1();
				}
			}
		}.start();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while (true) {
					op.out2();
				}
			}
		}.start();
	}
	

}

//實例方法中的同步代碼塊,鎖對象可以是任意對像,也可以是this或者字節碼對象,但是要保證多線程使用的鎖對象是同一個;
class OutPrint3{
	public Object ob = new Object();
	public void out1(){
		synchronized(ob){
			System.out.print("窗");
			System.out.print("前");
			System.out.print("明");
			System.out.print("月");
			System.out.print("光");
			System.out.print("\r\n");
		}
	}
	
	public void out2(){
		synchronized (ob) {
			System.out.print("疑");
			System.out.print("是");
			System.out.print("地");
			System.out.print("上");
			System.out.print("霜");
			System.out.print("\r\n");
		}
		
	}
}
  • 靜態方法中的同步代碼塊
public class SynchronizedDemo2 {
	
	public static void main(String[] args) {
		final OutPrint3 op = new OutPrint3();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while(true){
					op.out1();
				}
			}
		}.start();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while (true) {
					op.out2();
				}
			}
		}.start();
	}
	

}

//靜態方法中的同步代碼塊,鎖對象可以是靜態成員變量對象,也可以是字節碼對象;條件是多線程使用的鎖對象是同一把鎖;
class OutPrint3{
	public static Object ob = new Object();
	public static void out1(){
//		synchronized(ob){
		synchronized(OutPrint3.class){
			System.out.print("窗");
			System.out.print("前");
			System.out.print("明");
			System.out.print("月");
			System.out.print("光");
			System.out.print("\r\n");
		}
	}
	
	public void out2(){
//		synchronized (ob) {
		synchronized (OutPrint3.class) {
			System.out.print("疑");
			System.out.print("是");
			System.out.print("地");
			System.out.print("上");
			System.out.print("霜");
			System.out.print("\r\n");
		}
	}
}
  • 實例同步方法
public class SynchronizedDemo3 {
	
	public static void main(String[] args) {
		final OutPrint4 op = new OutPrint4();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while(true){
					op.out1();
				}
			}
		}.start();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				while (true) {
					op.out2();
				}
			}
		}.start();
	}
	

}

//實例同步方法,鎖對象是this,條件是多線程使用的鎖對象是同一把鎖,才能實現多線程同步
class OutPrint4{

	public synchronized void out1(){
		System.out.print("窗");
		System.out.print("前");
		System.out.print("明");
		System.out.print("月");
		System.out.print("光");
		System.out.print("\r\n");
	}
	
	public synchronized void out2(){
		System.out.print("疑");
		System.out.print("是");
		System.out.print("地");
		System.out.print("上");
		System.out.print("霜");
		System.out.print("\r\n");
	}
}
  • 靜態同步方法
public class SynchronizedDemo3 {
	
	public static void main(String[] args) {
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				int i = 0;
				while(true){
					++i;
					if(i<2000)
						OutPrint4.out1();
					else
						break;
				}
			}
		}.start();
		new Thread(){
			@Override
			public void run() {
				// TODO Auto-generated method stub
				super.run();
				int i = 0;
				while(true){
					++i;
					if(i<2000)
						OutPrint4.out2();
					else
						break;
				}
			}
		}.start();
	}
}

//靜態同步方法,鎖對象是字節碼對象,條件是多線程使用的鎖對象是同一把鎖,才能實現多線程同步;
class OutPrint4{

	public static synchronized void out1(){
		System.out.print("窗");
		System.out.print("前");
		System.out.print("明");
		System.out.print("月");
		System.out.print("光");
		System.out.print("\r\n");
	}
	
	public static synchronized void out2(){
		System.out.print("疑");
		System.out.print("是");
		System.out.print("地");
		System.out.print("上");
		System.out.print("霜");
		System.out.print("\r\n");
	}
}

以上四種主要實現多線程同步輸出打印功能,如下圖:

下圖是沒有實現多線程同步的輸出,可以看出代碼塊沒同步,導致輸出語序錯亂;

1.3.2,ReentrantLock鎖機制

1.3.3,特殊域變量volatile實現線程同步

1.3.4,ThreadLocal 局部變量實現線程同步

 

二,多線程安全

2.1,多線程併發操作同一數據時, 就有可能出現線程安全問題

2.2,解決辦法:

使用同步技術可以解決線程安全問題, 把操作數據的代碼進行同步, 不要多個線程一起操作;

2.3,多線程不安全案例:鐵路售票,一共100張,通過三個窗口賣完.

以下代碼有個問題就是:創建了三個窗口(線程)每個窗口都有自己的一百張票(ticketCount);所以要想三個窗口共享100張票,就需要把ticketCount成員變量改爲靜態的,所有Ticket對象共享;

public class Thread_ticket {
	
	public static void main(String[] args) {
		new Ticket("窗口1").start();
		new Ticket("窗口2").start();
		new Ticket("窗口3").start();
	}
}


class Ticket extends Thread{
	public int ticketCount = 100;
	
	public Ticket(String name) {
		super();
		this.setName(name);
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		while(ticketCount>0){
			System.out.println(this.getName()+"...出售第" + (100-ticketCount+1) +"張票");
			ticketCount--;
		}
	}
}

修改ticketCount成員變量爲靜態的:這樣一處改變處處變;

class Ticket extends Thread{
    
	public static int ticketCount = 100; //加上static修飾變成靜態變量,所有Ticket對象共享;
	
	public Ticket(String name) {
		super();
		this.setName(name);
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		while(ticketCount>0){
                try {
			    Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
            if(ticketCount>0){			
			    System.out.println(this.getName()+"...出售第" + (100-ticketCount+1) +"張  票");
			    ticketCount--;
            }
		}
	}
}

打印輸出:

原因分析:線程2賣第100張票的時候,執行到休眠方法休眠了;接着線程100也開始了賣第1張票,執行到休眠方法是也休眠;最後線程3也開始賣第100張票,執行到休眠方法開始休眠;這是線程2休眠結束賣了第100票,ticketCount--還沒執行,這是線程1休眠結束了也賣了第100張票,然後線程1和線程2都賣了第100張票結束時ticketCount= -1,所以等線程3休眠結束時,就賣了第102張票了;

以上就是多線程操作同一個數據時,出現了線程安全問題;

解決辦法:線程同步;例如通過同步代碼塊解決:

public class ThreadTicket3 {

	public static void main(String[] args) {
		new Ticket3("窗口1").start();
		new Ticket3("窗口2").start();
		new Ticket3("窗口3").start();
	}
}

class Ticket3 extends Thread {
	public static int ticketCount = 100; // 加上static修飾變成靜態變量,所有Ticket對象共享;

	public Ticket3(String name) {
		super();
		this.setName(name);
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		super.run();
		while (ticketCount > 0) {
			synchronized (Ticket3.class) {//Ticket.class字節碼對象保證了所有線程使用的時同一把鎖
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				if (ticketCount > 0) {
					System.out.println(this.getName() + "...出售第"
							+ (100 - ticketCount + 1) + "張票");
					ticketCount--;
				}
			}
		}

		//第二種邏輯
//		while (true) {
//			synchronized (Ticket3.class) {
//				if (ticketCount <= 0) {
//					break;
//				}
//				try {
//					Thread.sleep(10);
//				} catch (InterruptedException e) {
//					// TODO Auto-generated catch block
//					e.printStackTrace();
//				}
//				System.out.println(this.getName() + "...出售第"
//						+ (100 - ticketCount + 1) + "張票");
//				ticketCount--;
//			}
//		}
	}
}

總結,多線併發有可能會造成多線程安全問題,可以使用多線程同步技術解決線程安全問題;以上已經介紹了同步技術,接着介紹了多線程併發造成多線程安全問題及使用同步技術解決多線程安全問題;

 

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