Java學習——併發編程之鎖的深入化

五、鎖的深入化

鎖是併發編程共享數據,保證數據一致性的工具。在Java中有多種實現,例如synchronized(重量級鎖)、ReentrantLock(輕量級鎖)等,這些鎖爲我們的來發提供了便利。下面我跟大家聊一聊Java中鎖的相關知識。

1.重入鎖

重入鎖的概念:重入鎖也叫遞歸鎖。就是說同一線程中,外層函數獲取了鎖,可以傳遞給內層函數去使用,可重入性可以避險死鎖現象。synchronized(重量級鎖)、ReentrantLock(輕量級鎖)都屬於重入鎖。下面寫一個可重入鎖的例子:

class Test implements Runnable{
	@Override
	public void run() {
		set();
	}
	
	//synchronized要在代碼塊執行完畢後纔會釋放鎖
	public synchronized void set(){
		System.out.println("set方法");
		get();
	}
	
	public synchronized void get(){
		System.out.println("get方法");
	}
}

//synchronized(重量級) 和Lock鎖(輕量級)——重入鎖(具有遞歸性)
public class test01 {
	

	public static void main(String[] args) {
		
		Test test = new Test();
		
		Thread t1 = new Thread(test);
		
		t1.start();

	}
}

在這個例子中,set方法和get方法的鎖是同一個,在這裏我們假設synchronized鎖不具有可重入性,那麼get方法就必須要等待set方法釋放鎖後才能獲取鎖,這樣在set方法中調用get方法必然會造成死鎖現象(get方法一直在等待set方法執行完畢)。但是上面的代碼並沒有出錯,說明synchronized鎖具有可重入性,set方法中調用get方法,將set方法獲取的鎖傳遞給內層函數(get)。

2.讀寫鎖

假設程序中涉及到對一些共享資源的讀寫操作,並且在沒有做寫入操作是,允許兩個線程同時讀入資源。這時就需要用到讀寫鎖。讀寫鎖允許多個線程同時讀取資源,但是不允許多個線程同時進行寫入操作或者同時讀寫操作,也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存。下面寫一個讀寫鎖的例子:

package com.zhu.test;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class test03 {
	
	private volatile Map<String,String> caChe = new HashMap<>();
	
	//新建一個讀寫鎖
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	
	//寫鎖
	private WriteLock writeLock = rwl.writeLock();
	//讀鎖
	private ReadLock readLock = rwl.readLock();
	
	//寫入
	public void put(String key,String value){
		try {
			writeLock.lock();
			System.out.println("寫入put方法key:" + key + ",value:" + value + "開始");
			//Thread.sleep(50);
			caChe.put(key, value);
			System.out.println("寫入put方法key:" + key + ",value:" + value + "結束");
			
		} catch (Exception e) {
			// TODO: handle exception
		}finally {
			writeLock.unlock();
		}
	}
	
	//讀取
	public String get(String key){
		try {
			readLock.lock();
			String value = caChe.get(key);
			System.out.println("讀取get方法key:" + key + ",value:" + value + "開始");
			Thread.sleep(50);
			caChe.put(key, value);
			System.out.println("讀取get方法key:" + key + ",value:" + value + "結束");
			return value;
		} catch (Exception e) {
			// TODO: handle exception
		}finally {
			readLock.unlock();
		}
		return null;
	}
	
	public static void main(String[] args) {
		
		test03 t = new test03();
		
		//寫入線程
		Thread write = new Thread(new Runnable() {
			
			public void run() {
				for(int i = 0;i < 10;i++){
					t.put("i", i+"");
				}
				
			}
		});
		
		//讀取線程
		Thread read = new Thread(new Runnable() {
			
			public void run() {
				for(int i = 0;i < 10;i++){
					t.get("i");
				}
				
			}
		});
		
		write.start();
		
		read.start();
	}

}

實驗結果:

通過實驗結果,我們可以發現,在寫入資源時,我們調用寫鎖的lock()方法,寫入結束調用unlock()方法;在讀取資源時,我們調用讀鎖的lock()方法,寫入結束調用unlock()方法,並且在寫入操作沒有完成之前,是不能進行讀取操作的。

3.樂觀鎖/悲觀鎖

      樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。悲觀鎖認爲對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖的併發操作一定會出問題。在Java中,synchronized的思想也是悲觀鎖。

       樂觀鎖則認爲對於同一個數據的併發操作,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認爲,不加鎖的併發操作是沒有事情的。從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。悲觀鎖在Java中的使用,就是利用各種鎖樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

4.cas無鎖機制

CAS:Compare and Swap,即比較再交換。

jdk5增加了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。

5.CAS算法理解

(1)與鎖相比,使用比較交換(下文簡稱CAS)會使程序看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且,線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,因此,它要比基於鎖的方式擁有更優越的性能。

(2)無鎖的好處:

第一,在高併發的情況下,它比有鎖的程序擁有更好的性能;

第二,它天生就是死鎖免疫的。

就憑藉這兩個優勢,就值得我們冒險嘗試使用無鎖的併發。

(3)CAS算法的過程是這樣:它包含三個參數CAS(V,E,N): V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,如果V值和E值不同,則說明已經有其他線程做了更新,則將主內存的值刷新到本地內存,再去做比較,一直重試。最後,CAS返回當前V的真實值。

V=需要更新變量,主內存

E=預望值,本地內存

N=新值

如果V=E(主內存值與本地內存值一致),說明:沒有被修改過,將V的值設置爲N。

如果V!=E(主內存值與本地內存值不一致),已經被修改。

(4)CAS操作是抱着樂觀的態度進行的,它總是認爲自己可以成功完成操作。當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的線程不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他線程對當前線程的干擾,並進行恰當的處理。

(5)簡單地說,CAS需要你額外給出一個期望值,也就是你認爲這個變量現在應該是什麼樣子的。如果變量不是你想象的那樣,那說明它已經被別人修改過了。你就重新讀取,再次嘗試修改就好了。

(6)在硬件層面,大部分的現代處理器都已經支持原子化的CAS指令。在JDK 5.0以後,虛擬機便可以使用這個指令來實現併發操作和併發數據結構,並且,這種操作在虛擬機中可以說是無處不在。

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