Java 高併發系列2-併發鎖

Java 高併發系列2-併發鎖

接着上一篇併發文章我們繼續
Java 高併發系列1-開篇

本篇的主要內容是以下幾點:

  • wait 、notify 的簡單使用
  • Reentrantlock的簡單使用
  • synchronized 與Reentrantlock的區別
  • ThreadLocal的簡單使用

看一個面試題:

曾經的面試題:(淘寶?)
實現一個容器,提供兩個方法,add,size
寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數到5個時,線程2給出提示並結束

public class MyContainer1 {

   /* volatile */	List lists = new ArrayList(); 

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}
	
	public static void main(String[] args) {
		MyContainer1 c = new MyContainer1();

		new Thread(() -> {
			for(int i=0; i<10; i++) {
				c.add(new Object());
				System.out.println("add " + i);
				
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "t1").start();
		
		new Thread(() -> {
			while(true) {
				if(c.size() == 5) {
					break;
				}
			}
			System.out.println("t2 結束");
		}, "t2").start();
	}
}

看完大概實現方法就是 使用while(true) 死循環 進行讀取容器的大小,如果不添加volatile 關鍵字 t2 沒有辦法跳出while循環,因爲容器的改變對 t2不可見。
添加volatile 字段可見之後,當然可以實現對容器大小的監聽。
但是也存在兩個問題,
第一、浪費CPU,在t1執行到5之前CPU都是在空轉。
第二、不夠精準,在循環判斷容器大小==5時 跳出循環的時候 可能容器的大小已經添加到了6 或者7 。

爲了解決這個問題, 我們再來看一條程序,

public class MyContainer3 {

	//添加volatile,使t2能夠得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}
	
	public static void main(String[] args) {
		MyContainer3 c = new MyContainer3();
		
		final Object lock = new Object();
		
		new Thread(() -> {
			synchronized(lock) {
				System.out.println("t2啓動");
				if(c.size() != 5) {
					try {//// 掛起程序 釋放鎖 lock 等待。 
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("t2 結束");
			}
			
		}, "t2").start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1啓動");
			synchronized(lock) {
				for(int i=0; i<10; i++) {
					c.add(new Object());
					System.out.println("add " + i);
					
					if(c.size() == 5) {
						lock.notify();
						//// 獲取鎖 lock 等待, 當size ==5  lock.notify 喚醒 t2
					}
					
					try {
						TimeUnit.SECONDS.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}, "t1").start();
		
		
	}
}

看完程序 這裏使用wait和notify做到,wait會釋放鎖,而notify不會釋放鎖
也可以 在 t1 notify之後,t1必須釋放鎖,t2退出後,也必須notify,通知t1繼續執行

缺點: 整個通信過程比較繁瑣

需要注意的是,運用這種方法,必須要保證t2先執行,也就是首先讓t2監聽纔可以。

再看一條程序

public class MyContainer5 {

	// 添加volatile,使t2能夠得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}

	public static void main(String[] args) {
		MyContainer5 c = new MyContainer5();

		CountDownLatch latch = new CountDownLatch(1);

		new Thread(() -> {
			System.out.println("t2啓動");
			if (c.size() != 5) {
				try {
					latch.await();
					
					//也可以指定等待時間
					//latch.await(5000, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("t2 結束");

		}, "t2").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1啓動");
			for (int i = 0; i < 10; i++) {
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
					// 打開門閂, 拉閘放水, 讓t2得以執行
					latch.countDown();
				}

				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

		}, "t1").start();

	}
}

使用CountDownLatch (門閂)替代wait notify來進行通知 好處是通信方式簡單,同時也可以指定等待時間 使用await和countdown方法替代wait和notify CountDownLatch不涉及鎖定,當count的值爲零時當前線程繼續運行 當不涉及同步,只是涉及線程通信的時候,用synchronized + wait/notify就顯得太重了

這時應該考慮 countdownlatch/cyclicbarrier/semaphore

接下來再看一下Reentrantlock

程序1

public class ReentrantLock1 {
	synchronized void m1() {
		for(int i=0; i<10; i++) {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(i);
		}
		
	}
	
	synchronized void m2() {
		System.out.println("m2 ...");
	}
	
	public static void main(String[] args) {
		ReentrantLock1 rl = new ReentrantLock1();
		new Thread(rl::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(rl::m2).start();
	}
}

Reentrantlock用於替代synchronized , 由於m1鎖定this,只有m1執行完畢的時候,m2才能執行 這也是synchronized最原始的意義。

程序2
先簡單註釋一下

public class ReentrantLock2 {
    /// 聲明鎖
	Lock lock = new ReentrantLock();

	void m1() {
		try {
		    /// 上鎖 ,相當於synchronized(this)
			lock.lock(); //synchronized(this)
			for (int i = 0; i < 10; i++) {
				TimeUnit.SECONDS.sleep(1);

				System.out.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
		///// 這裏的try{}finally{}  // 一定要加  , 在finally塊中 釋放鎖。 
			lock.unlock();
		}
	}

	void m2() {
		lock.lock();
		System.out.println("m2 ...");
		lock.unlock();
	}

	public static void main(String[] args) {
		ReentrantLock2 rl = new ReentrantLock2();
		new Thread(rl::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(rl::m2).start();
	}
}

這裏不再過多解釋, 看註釋。
由於synchronized 是碰見異常 jvm自動釋放鎖。 而 ReentrantLock不行, 需要手動釋放。 所以一般情況下 放在finally語句裏。
** 重要的事情說三遍, 必須手動釋放, 手動釋放。必須手動釋放。 **

程序3.

public class ReentrantLock3 {
/// 聲明
	Lock lock = new ReentrantLock();

	void m1() {
		try {
		/// 鎖
			lock.lock();
			for (int i = 0; i < 10; i++) {
				TimeUnit.SECONDS.sleep(1);

				System.out.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
		/// 釋放
			lock.unlock();
		}
	}

	/**
	 * 使用tryLock進行嘗試鎖定,不管鎖定與否,方法都將繼續執行
	 * 可以根據tryLock的返回值來判定是否鎖定
	 * 也可以指定tryLock的時間,由於tryLock(time)拋出異常,所以要注意unclock的處理,必須放到finally中
	 */
	void m2() {
		/*
		boolean locked = lock.tryLock();
		System.out.println("m2 ..." + locked);
		if(locked) lock.unlock();
		
		<!--locked 後邊這裏可以根據是否鎖定來選擇執行相關邏輯-->
		*/
		
		boolean locked = false;
		
		try {
			locked = lock.tryLock(5, TimeUnit.SECONDS);
			//////// 可以嘗試鎖定,等待5秒鐘, 超時後鎖定失敗,返回false
			System.out.println("m2 ..." + locked);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
	
			if(locked) lock.unlock();
		}
		
	}

	public static void main(String[] args) {
		ReentrantLock3 rl = new ReentrantLock3();
		new Thread(rl::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(rl::m2).start();
	}
}

看起來是不是ReentrantLock 是不是高級很多,繼續
下一條程序


public class ReentrantLock4 {
		
	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		
		Thread t1 = new Thread(()->{
			try {
				lock.lock();
				System.out.println("t1 start");
				TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
				//// 1. 睡眠不釋放鎖, 2. 睡眠這麼長時間,相當於睡死了都, 看下個線程。 
				System.out.println("t1 end");
			} catch (InterruptedException e) {
				System.out.println("interrupted!");
			} finally {
				lock.unlock();
			}
		});
		t1.start();
		
		Thread t2 = new Thread(()->{
			try {
				//lock.lock();
				///// 顧名思義就是 把鎖打斷, 打斷線程1的等待 
				lock.lockInterruptibly(); //可以對interrupt()方法做出響應
				System.out.println("t2 start");
				TimeUnit.SECONDS.sleep(5);
				System.out.println("t2 end");
			} catch (InterruptedException e) {
				System.out.println("interrupted!");
			} finally {
				lock.unlock();
			}
		});
		t2.start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		t2.interrupt(); //打斷線程2的等待  
		
	}
}

lockInterruptibly()獲取鎖是以排他的模式獲取,一旦被中斷就放棄等待獲取, 可以對線程interrupt方法做出響應,在一個線程等待鎖的過程中,可以被打斷。

ReentrantLock 除了是可重入鎖 還可以設置公平鎖和非公平鎖。再來一條程序

public class ReentrantLock5 extends Thread {
		
	private static ReentrantLock lock=new ReentrantLock(true); //參數爲true表示爲公平鎖,請對比輸出結果
    public void run() {
        for(int i=0; i<100; i++) {
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"獲得鎖");
            }finally{
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        ReentrantLock5 rl=new ReentrantLock5();
        Thread th1=new Thread(rl);
        Thread th2=new Thread(rl);
        th1.start();
        th2.start();
    }
}

根據參數true 或者 false ,是否可以設定爲公平鎖。
所謂的公平鎖設定算法爲 調度器優先選擇等待時間長的線程執行。
而非公平鎖則沒有該設定。

再來看看ThreadLocal , 顧名思義, ThreadLocal線程局部變量
來一條程序,

public class ThreadLocal1 {
    //// 聲明person 對象 volatile ,
	volatile static Person p = new Person();
	
	public static void main(String[] args) {
				
		new Thread(()->{
			try {
			//// 睡2秒
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			/// 打印
			System.out.println(p.name);
		}).start();
		
		new Thread(()->{
			try {
			/// 睡1秒
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 賦值
			p.name = "lisi";
		}).start();
	}
}

class Person {
	String name = "zhangsan";
}

打印結果 lisi , 線程1 睡2秒, 睡醒後根據 volatile關鍵字特性, 線程修改 對其他線程可見, 既可以讀取到 線程2 修改後的值。

再來一條,


public class ThreadLocal2 {
	//volatile static Person p = new Person();
	///// 將聲明 封裝了Person的ThreadLocal對象  
	static ThreadLocal<Person> tl = new ThreadLocal<>();
	
	public static void main(String[] args) {
				
		new Thread(()->{
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			System.out.println(tl.get());
		}).start();
		
		new Thread(()->{
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			tl.set(new Person());
		}).start(); 
	}
	
	static class Person {
		String name = "zhangsan";
	}
}

由於線程1 線程2 都是讀取自己Thread 對應的ThreadLocalMap 對象。
所以 打印結果 就是 當前線程取到的值 null 。

在Handler消息機制中的應用就是 Looper 通過 ThreadLocal 與currentThread 綁定了,所以才實現了通過Handler sendMessage 到指定線程。 如果想要詳細瞭解ThreadLocal的使用原理 移步我以前寫的一篇文章。
ThreadLocal源碼詳細解析
還有 在hibernate中session就存在與ThreadLocal中,避免synchronized的使用

好了, 囉裏囉嗦,說了一大通,看的雲裏霧裏。 其實我覺得如果能把代碼拿出來 敲一下,跑一跑,應該就會明白使用多線程和鎖的妙處。 東西比較多,如果有什麼不對的,請批評指正。 這篇就先說到這裏,下篇我們再見。

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