理解 JVM:Java 內存模型(三)—— 鎖

什麼是線程安全?

線程安全是指當一個對象被多個線程訪問操做時,最終都能獲得正確的結果,那這個對象是線程安全的。Java 中能夠經過同步塊和加鎖保障線程安全。java


鎖是 Java 併發編程中最重要的同步機制,它可讓等待在臨界區的線程互斥執行。編程

關鍵字 synchronized

synchronized 也是一種鎖,它經過字節碼指令 monitorenter 和 monitorexist 隱式的來使用 lock 和 unlock 操做。synchronized 具備原子性和可見性。安全

synchronized 示例:多線程

/**
 * @Project Name:test
 * @File Name:SynchronizedTest.java
 * @Package Name:com.pdh
 * @Date:2017年2月12日下午5:36:46
 */

package com.pdh.test.thread;

/**
 * synchronized 具備原子性和可見性
 *
 * @author pengdh
 * @date 2017/11/12
 */
public class SynchronizedDemo {

	private int safeNum = 0;
	private int unsafeNum = 0;

	/**
	 * 使用 synchronized 同步實現複合運算,線程安全的
	 */
	private synchronized void safeIncrease() {
		safeNum++;
	}

	/**
	 * 普通複合運算,線程不安全的
	 */
	private void unsafeIncrease() {
		unsafeNum++;
	}

	public static void main(String[] args) {
		SynchronizedDemo demo = new SynchronizedDemo();
		for (int i = 0; i < 20000; i++) {
			new Thread(() -> {
				demo.unsafeIncrease();
				demo.safeIncrease();
			}).start();
		}

		while (Thread.activeCount() > 2) {
			Thread.yield();
		}

		System.out.println("unsafeNum: " + demo.unsafeNum);
		System.out.println("safeNum: " + demo.safeNum);
	}
}

在示例中,申明瞭兩個方法,一個是使用了 synchronized 修飾的同步塊方法 safeIncrease() 對共享變量 safeNum 的自增操做,因爲該方法使用了 synchronized 同步塊實現了線程對變量 safeNum 的互斥操做,是線程安全的,因此最終獲取到了正確的結果 20000;而另外一個方法 unsafeIncrease() 爲使用任何手段保護 全局變量 unsafeNum,是線程不安全的,因此最終獲取到的結果大多數狀況下是小於 20000 的。併發

重入鎖(ReentrantLock)

從 JDK 5.0 開始,Java 併發包實現了高性能的支持重入的鎖 ReentrantLock,屬於一種排它鎖。重入鎖經過顯示的請求獲取和釋放鎖,爲了不得到鎖後,沒有釋放鎖,而形成其它線程沒法得到鎖而形成死鎖,通常建議將釋放鎖操做放在finally塊裏。性能

重入鎖示例:ui

package com.pdh.test.thread.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * ReentrantLock 實現線程同步
 *
 * @author pengdh
 * @date 2017/11/12
 */
public class ReentrantLockDemo implements Runnable {
	private int num = 0;
	private Lock lock = new ReentrantLock();

	/**
	 * 利用重入鎖實現變量累加操做
	 */
	private void increase() {
		lock.lock();
		try {
			num++;
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) {
		ReentrantLockDemo demo = new ReentrantLockDemo();
		for (int i = 0; i < 2000; i++) {
			new Thread(() -> {
				demo.increase();
			}).start();
		}
		// 等待全部線程所有執行完
		while (Thread.activeCount() > 2) {
			Thread.yield();
		}
		System.out.println(demo.num);
	}
}

重入鎖提供如下幾個主要方法spa

  • lock():得到鎖,若是鎖已經被佔用,則等待;
  • lockInterruptibly():獲取鎖,若獲取成功當即返回;若獲取不成功則阻塞等待。與lock方法不一樣的是,在阻塞期間,若是當前線程被打斷(interrupt)則該方法拋出InterruptedException。該方法能夠有效避免死鎖題。
  • tryLock():該方法嘗試獲取鎖,若是成功,返回 true ,若失敗返回 false。該方法不等待,當即返回。
  • tryLock(long time, TimeUnit unit):該方法嘗試獲取鎖,若成功,則返回 true,若失敗,則等待相應的時間,若是該時間內能得到鎖,則返回 true,若是相應的時間內仍未獲取到鎖則返回 false,若是在等待期間當前線程被中斷則拋出 InterruptedException;
  • unlock():釋放鎖;

synchronized 與 ReentrantLock 的區別

  1. 全部使用 synchronized 的地方均可以使用 ReentrantLock ;
  2. synchronized 是經過字節碼指令 monitorenter 和 monitorexist 實現隱式加鎖和解鎖,而 ReentrantLock 是經過 lock 和 unlock 操做實現顯示加鎖和解鎖;
  3. ReentrantLock 能夠實現公平和非公平兩種鎖,而 synchronized 只能實現非公平鎖;
  4. ReentrantLock 能夠中斷對鎖的等待來避免 synchronized 可能帶來的死鎖問題;

Condition 條件鎖

Condition 的用法與 wait() 和 notify() 做用相似,wait() 和 notify() 是和 synchronized 關鍵字搭配使用,而 Condition 是與重入鎖搭配使用。利用 Condition 對象,就可讓線程在合適的時間等待,或者在某個特定的時刻獲得通知,繼續執行。線程

Condition 提供如下幾個方法code

  • await():該方法會使當前線程等待,同時釋放當前鎖,當其它線程中使用 signal() 或者 singnalAll() 方法時,線程會從新得到鎖並繼續執行。或者當前線程被中斷時,也能跳出等待。
  • awaitUninterruotibly():該方法使用和 await() 基本相同,可是它不會再等待的過程當中響應中斷。
  • singal():該方法用於喚醒一個在等待中的線程,而 singalAll() 方法會喚醒全部在等待的方法。

Condition 示例:

package com.pdh.test.thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Condition 示例
 *
 * @author pengdh
 * @date 2017/11/12
 */
public class ConditionDemo {

	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		Condition condition = lock.newCondition();
		new Thread(() -> {
			lock.lock();
			System.out.println("thread 1 is waiting");
			try {
				condition.await();
				System.out.println("thread 1 is wake up");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}).start();

		new Thread(() -> {
			lock.lock();
			try {
				System.out.println("thread 2 is running");
				Thread.sleep(3000);
				condition.signal();
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		}).start();
	}
}

信號量(Semaphore)

信號量爲多線程協做提供了更爲強大的控制方法,廣義上說,信號量是對鎖的擴展。不管是內部鎖 synchronized 仍是重入鎖 ReentrantLock,一次都只容許一個線程訪問一個資源,而信號量卻能夠指定多個線程,同時訪問某一個資源。

信號量提供瞭如下方法

  • acquire():該方法嘗試得到一個許可。若沒法得到,則線程會等待,直到有線程釋放一個許可或者當前線程被中斷。
  • acquire(int permits): 申請 permits(必須爲非負數)個許可,若獲取成功,則該方法返回而且當前可用許可數減permits;若當前可用許可數少於 permits 指定的個數,則繼續等待可用許可數大於等於 permits;若等待過程當中當前線程被中斷,則拋出 InterruptedException。
  • acquireUninterruptibly() :該方法和 acquire() 相似,但不響應中斷。
  • tryAcquire():嘗試得到一個許可 ,若是成功則返回 true,若是失敗則返回 false,它不會進行等待,當即返回。
  • release():用於在線程訪問資源結束後,釋放一個許可,以使其它等待許可的線程能夠進行資源訪問。

Semaphore 示例:

package com.pdh.test.thread;

import java.util.concurrent.Semaphore;

/**
 * 信號量示例
 *
 * @author pengdh
 * @date 2017/11/12
 */
public class SemaphoreDemo {
	private static final Semaphore semaphoreToken = new Semaphore(5);

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			new Thread(() -> {
				// 獲取令牌
				try {
					semaphoreToken.acquire();
					Thread.sleep(2000);
					System.out.println(Thread.currentThread().getId() + " finished");
					// 歸還令牌
					semaphoreToken.release();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}).start();
		}
	}
}

讀寫鎖(ReadWriteLock)

讀寫鎖適用於多線程編程中讀多寫少的場景中,對於原子性而言,更可能是關注寫操做,讀和讀操做之間並不須要互斥操做,只須要保證讀和寫或者寫和寫之間是互斥操做便可。因此若是在某個場景下讀操做次數遠遠大於寫操做次數,則可使用讀寫鎖來提升效率。

讀寫鎖示例

package com.pdh.test.thread;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 讀寫鎖示例
 *
 * @author pengdh
 * @date 2017/11/13
 */
public class ReadWriteLockDemo {

	public static void main(String[] args) {
		ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
		new Thread(() -> {
			readWriteLock.readLock().lock();
			try {
				System.out.println("the first read lock begin");
				Thread.sleep(1000);
				System.out.println("the first read lock end");
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				readWriteLock.readLock().unlock();
			}
		}).start();

		new Thread(() -> {
			readWriteLock.readLock().lock();
			try {
				System.out.println("the second read lock begin");
				Thread.sleep(1000);
				System.out.println("the second read lock end");
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				readWriteLock.readLock().unlock();
			}
		}).start();

		new Thread(() -> {
			readWriteLock.writeLock().lock();
			try {
				System.out.println("the write lock begin");
				Thread.sleep(1000);
				System.out.println("the write read lock end");
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				readWriteLock.writeLock().unlock();
			}
		}).start();
	}
}

參考文獻

  • <span style="color:red;font-size:14px;font-family:Microsoft YaHei;">深刻理解 Java 虛擬機</span>

歡迎掃一掃關注 程序猿pdh 公衆號!

歡迎關注公衆號 程序猿pdh

相關文章
相關標籤/搜索