多線程編程之synchronized和Lock
在高並發多線程應用場景中對於synchronized和Lock的使用是很普遍的,這篇文章我們就來進行這些知識點的學習,比如說:公平鎖與非公平鎖、樂觀鎖與悲觀鎖、線程間通信、讀寫鎖、數據臟讀等知識內容。
目錄:
1.同步問題的產生與案例代碼
2.synchronized解決同步問題
3.Lock解決同步代碼問題
4.公平鎖與非公平鎖
5.樂觀鎖與悲觀鎖
6.synchronized與Lock比較
同步問題案例
這個問題在我們日常生活中非常常見,比如說:秒殺物品的庫存數據、火車票剩余票等就是有同步問題,下面我們通過代碼來解釋這個問題產生的原理:
package com.ckmike.mutilthread; import java.util.concurrent.TimeUnit; /** * SynchronizedQuestionDemo 簡要描述 * <p> TODO:描述該類職責 </p> * * @author ckmike * @version 1.0 * @date 18-12-21 下午1:34 * @copyright ckmike **/ public class SynchronizedQuestionDemo { public static void main(String[] args) { // 只有10張票 TicketService ticketService = new TicketService(10); Thread buy1 = new Thread(ticketService); buy1.setName("buy1"); Thread buy2 = new Thread(ticketService); buy2.setName("buy2"); Thread buy3 = new Thread(ticketService); buy3.setName("buy3"); Thread buy4 = new Thread(ticketService); buy4.setName("buy4"); buy1.start(); buy2.start(); buy3.start(); buy4.start(); } } class TicketService implements Runnable{ private int ticket_store; public TicketService(int ticket_store) { this.ticket_store = ticket_store; } @Override public void run() { while (true) { if (ticket_store > 0) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 輸出賣票信息 System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--); }else{ break; } } } }
上面的執行結果明顯是不符合我們的預期的,四個線程同時去買票,對余票數據的判斷存在問題,這就是數據臟讀的場景。
解決辦法:在做售票這個操作時,對於ticket_store的操作同一時刻只能一個線程操作,那麽我們這裏就會用到鎖這個概念了,對於共享數據java中解決數據臟讀可以通過synchronized和Lock去解決。
現在我們帶著這個問題來了解synchronized和Lock。
synchronized
JVM中每個對象都有一個監控器可以作為鎖。當線程試圖訪問同步代碼時,必須先獲得對象鎖(對象監視器),退出或拋出異常時必須釋放鎖。Synchronzied實現同步的表現形式分為:代碼塊同步和方法同步。
同步代碼塊
在編譯後通過將Monitor Enter指令插入到同步代碼塊的開始處,將Monitor Exit指令插入到方法結束處和異常處,通過反編譯字節碼可以觀察到。任何一個對象都有一個Monitor(對象監控器)與之關聯,線程執行Monitor Enter指令時,會嘗試獲取對象對應的monitor的所有權,即嘗試獲得對象鎖。
同步方法
從class文件結構中可知,synchronized方法在method_info結構有ACC_synchronized標記,線程執行時會識別該標記,獲取對應的對象鎖,實現方法同步。
雖然同步方法和同步代碼塊實現細節不同,但本質上都是對一個對象監視器(monitor)的獲取(對象鎖的獲取)。任意一個對象都擁有自己的監視器,當同步代碼塊或同步方法時,執行方法的線程必須先獲得該對象的監視器才能進入同步塊或同步方法,沒有獲取到監視器的線程將會被阻塞,並進入同步隊列,狀態變為BLOCKED。當成功獲取監視器的線程釋放了鎖後,會喚醒阻塞在同步隊列的線程,使其重新嘗試對監視器的獲取。
synchronized解決數據臟讀問題
package com.ckmike.mutilthread;
import java.util.concurrent.TimeUnit;
/**
* SynchronizedQuestionDemo 簡要描述
* <p> TODO:描述該類職責 </p>
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午1:34
* @copyright ckmike
**/
public class SynchronizedQuestionDemo {
public static void main(String[] args) {
// 只有10張票
TicketService ticketService = new TicketService(10);
Thread buy1 = new Thread(ticketService);
buy1.setName("buy1");
Thread buy2 = new Thread(ticketService);
buy2.setName("buy2");
Thread buy3 = new Thread(ticketService);
buy3.setName("buy3");
Thread buy4 = new Thread(ticketService);
buy4.setName("buy4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
class TicketService implements Runnable{
private int ticket_store = 100;
public TicketService(int ticket_store) {
this.ticket_store = ticket_store;
}
@Override
public void run() {
while (true) {
if (ticket_store > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
if(ticket_store > 0) {
// 輸出賣票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
}else {
break;
}
}
}else {
break;
}
}
}
}
上面的方式是使用同步代碼塊實現的同步,解決了數據臟讀的問題。我們也可以通過同步方法來解決這個問題,相比較同步方法,同步代碼塊效率要高些。
上面的代碼我們是對於同一個TicketService實例進行多線程操作,所以可以達到同步效果,如果我們使用的是四個不同的實例,那麽他們之間就不再是互斥的,因為Java中的鎖是對象鎖,不同實例對象鎖是不一樣的,可自行驗證。
如果是靜態同步方法,那麽獲取的應該是該類的鎖,鎖住的是該類,當所有該類的對象(多個對象)在不同線程中調用這個static同步方法時,線程之間會形成互斥,達到同步效果。
結合上面思考:同步實例方法,同步類方法,synchronized(this),synchronized(ClassName.class)他們之間的一個關系就出來了,以及他們的應用場景也就出來了。
synchronized線程間通信問題
場景描述:現在我有三個線程分別為線程A,線程B,和線程C,三個線程之間有先後順序的,A操作完了,B才可以操作,B操作完了,C才可以操作。那麽這個時候就需要進行線程之間的通信,然線程知道什麽時候該自己執行。
分析:在線程A執行期間,B線程一直等待A的通知,B執行期間,C一直等待B的通知。
package com.ckmike.mutilthread;
/**
* BackupDemo 簡要描述
* <p> TODO:描述該類職責 </p>
*
* @author ckmike
* @version 1.0
* @date 18-12-20 下午1:51
* @copyright ckmike
**/
public class BackupDemo {
public static void main(String[] args) {
DataTool dataTool = new DataTool();
BackUpA A = new BackUpA(dataTool);
BackUpB B = new BackUpB(dataTool);
BackUpC C = new BackUpC(dataTool);
A.start();
B.start();
C.start();
}
}
class BackUpA extends Thread{
private DataTool dataTool;
public BackUpA(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2A();
}
}
class BackUpB extends Thread{
private DataTool dataTool;
public BackUpB(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2B();
}
}
class BackUpC extends Thread{
private DataTool dataTool;
public BackUpC(DataTool dataTool){
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2C();
}
}
class DataTool{
volatile public String prevA = "A";
// 備份到A數據源
synchronized public void backup2A(){
try {
while ("C".equals(prevA)) {
wait();
}
for(int i=0; i<2;i++){
System.out.println("backup2A數據源");
}
prevA = "B";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
// 備份到B數據源
synchronized public void backup2B(){
try{
while ("A".equals(prevA)){
wait();
}
for (int i=0; i<2; i++){
System.out.println("backup2B數據源");
}
prevA = "C";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
// 備份到c數據源
synchronized public void backup2C(){
try{
while ("B".equals(prevA)){
wait();
}
for (int i=0; i<2; i++){
System.out.println("backup2C數據源");
}
prevA = "C";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
}
通過volatile和synchronized和wait()\notifyAll()結合實現線程間通信,借助標識進行線程順序執行。
ReentrantLock
在Java中鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但有的鎖可以允許多個線程並發訪問共享資源,比如讀寫鎖,後面我們會分析)。在Lock接口出現之前,Java程序是靠synchronized關鍵字(後面分析)實現鎖功能的,而JAVA SE5.0之後並發包中新增了Lock接口用來實現鎖的功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖,缺點就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。
ReentrantLock解決同步問題
package com.ckmike.mutilthread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* SynchronizedQuestionDemo 簡要描述
* <p> TODO:描述該類職責 </p>
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午1:34
* @copyright ckmike
**/
public class SynchronizedQuestionDemo {
public static void main(String[] args) {
// 只有10張票
TicketService ticketService = new TicketService(10);
Thread buy1 = new Thread(ticketService);
buy1.setName("buy1");
Thread buy2 = new Thread(ticketService);
buy2.setName("buy2");
Thread buy3 = new Thread(ticketService);
buy3.setName("buy3");
Thread buy4 = new Thread(ticketService);
buy4.setName("buy4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
class TicketService implements Runnable{
private int ticket_store = 100;
// 默認是非公平鎖
private Lock lock = new ReentrantLock();
public TicketService(int ticket_store) {
this.ticket_store = ticket_store;
}
@Override
public void run() {
while (true) {
if (ticket_store > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
if(ticket_store > 0) {
// 輸出賣票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
}else {
break;
}
lock.unlock();
}else {
break;
}
}
}
}
使用Lock同樣可以解決數據多線程同步問題。
關於ReentrantLock的使用很簡單,只需要顯示調用,獲得同步鎖,釋放同步鎖即可。
ReentrantLock線程間通信
package com.ckmike.mutilthread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* BackupDemo 簡要描述
* <p> TODO:描述該類職責 </p>
*
* @author ckmike
* @version 1.0
* @date 18-12-20 下午1:51
* @copyright ckmike
**/
public class BackupDemo {
public static void main(String[] args) {
DataTool dataTool = new DataTool();
BackUpA A = new BackUpA(dataTool);
BackUpB B = new BackUpB(dataTool);
BackUpC C = new BackUpC(dataTool);
A.start();
B.start();
C.start();
}
}
class BackUpA extends Thread{
private DataTool dataTool;
public BackUpA(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2A();
}
}
class BackUpB extends Thread{
private DataTool dataTool;
public BackUpB(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2B();
}
}
class BackUpC extends Thread{
private DataTool dataTool;
public BackUpC(DataTool dataTool){
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2C();
}
}
class DataTool{
volatile public String prevA = "A";
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 備份到A數據源
public void backup2A(){
try {
lock.lock();
while ("C".equals(prevA)) {
condition.await();
}
for(int i=0; i<2;i++){
System.out.println("backup2A數據源");
}
prevA = "B";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 備份到B數據源
public void backup2B(){
try{
lock.lock();
while ("A".equals(prevA)){
condition.await();
}
for (int i=0; i<2; i++){
System.out.println("backup2B數據源");
}
prevA = "C";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 備份到c數據源
public void backup2C(){
try{
lock.lock();
while ("B".equals(prevA)){
condition.await();
}
for (int i=0; i<2; i++){
System.out.println("backup2C數據源");
}
prevA = "C";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
利用Condition可以實現與wait()\notifyAll()一樣的功能。Condition.await()等同於wait(),condition.signalAll()等同與notifyAll()。
公平鎖與非公平鎖
公平鎖:按照線程獲取鎖的順序來分配,FIFO。
非公平鎖:是一種獲取鎖的搶占機制,是隨機獲取鎖。可能造成某些線程一直拿不到鎖。
synchronized是非公平鎖,ReentrantLock可以通過isFair設置為公平鎖,默認是非公平鎖。
樂觀鎖與悲觀鎖
樂觀鎖與悲觀鎖的概念不是JAVA的概念,而是針對關系型數據庫數據更新時的一種解決方案。
樂觀鎖:就是認為數據沖突的可能性比較小,只有當事物提交時才回去判斷是否在讀取數據後,是否有其他事務修改了該數據,如果有,則當前事務進行回滾。可以這樣簡單理解:一條數據其中有一個字段是version,每次的更新操作都會自動+1,當你讀出這條數據後,如果有其他事務修改了,那麽version就與你提交的version不相等,那麽這個事務就不會被提交。
悲觀鎖:則認為數據沖突是大概率事件,所以每次進行修改之前都會先獲取該數據的鎖,類似於synchronized,所以花費時間較多,效率就會比較低。悲觀鎖是由數據庫自己實現了的,要用的時候,我們直接調用數據庫的相關語句就可以了。而悲觀鎖又分為共享鎖和排他鎖。
共享鎖:就是對於多個不同的事務,對同一個資源共享同一個鎖,類似於一個門多把鑰匙。
排它鎖:排它鎖與共享鎖相對應,類似於一個門只有一把鑰匙。
行鎖:這個就是字面上的意思,給數據行加上鎖。比如:SELECT * from user where id = 1 lock in share mode; 就是對id=1的數據行加了鎖。這個鎖就是行鎖。
表鎖:給表加上鎖。
這一部分應該是數據庫中的概念,我放到這裏就是因為曾經因為面試問得我一臉懵逼,所以就在這裏簡單的介紹一下,我後面還會寫關於數據庫關於鎖,索引、事務隔離等相關的文章。
ReentrantReadWriteLock
關於ReentrantLock進行更細粒度的鎖,就是這個ReentrantReadWriteLock讀寫鎖,可以針對讀和寫進行加鎖。特別要註意:只有讀讀是不用加鎖,屬於讀讀共享;但是只要有寫就一定要加鎖互斥,比如讀寫互斥,寫讀互斥,寫寫互斥。
讀寫案例:
package com.ckmike.mutilthread;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* ReadWriteDemo 簡要描述
* <p> TODO:描述該類職責 </p>
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午4:48
* @copyright ckmike
**/
public class ReadWriteDemo {
public static void main(String[] args) {
ReadWriteService readWriteService = new ReadWriteService();
ReadThread read1 = new ReadThread(readWriteService);
ReadThread read2 = new ReadThread(readWriteService);
read1.setName("A");
read2.setName("B");
read1.start();
read2.start();
WriteThread write1 = new WriteThread(readWriteService);
WriteThread write2 = new WriteThread(readWriteService);
write1.setName("C");
write2.setName("D");
write1.start();
write2.start();
}
}
class ReadThread extends Thread{
private ReadWriteService readWriteService;
public ReadThread(ReadWriteService readWriteService) {
this.readWriteService = readWriteService;
}
@Override
public void run() {
super.run();
readWriteService.read();
}
}
class WriteThread extends Thread{
private ReadWriteService readWriteService;
public WriteThread(ReadWriteService readWriteService) {
this.readWriteService = readWriteService;
}
@Override
public void run() {
super.run();
readWriteService.write();
}
}
class ReadWriteService{
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
try {
lock.readLock().lock();
System.out.println("獲取讀鎖:"+Thread.currentThread().getName()+" time:" +new Date().getTime());
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("獲取寫鎖:"+Thread.currentThread().getName()+" time:" +new Date().getTime());
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
重入鎖
當一個線程得到一個對象後,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次獲取自己的內部鎖。
Java裏面內置鎖(synchronized)和Lock(ReentrantLock)都是可重入的。
public class SynchronizedTest {
public void method1() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1獲得ReentrantTest的鎖運行了");
method2();
}
}
public void method2() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1裏面調用的方法2重入鎖,也正常運行了");
}
}
public static void main(String[] args) {
new SynchronizedTest().method1();
}
}
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("方法1獲得ReentrantLock鎖運行了");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法1裏面調用的方法2重入ReentrantLock鎖,也正常運行了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockTest().method1();
}
}
synchronized與ReentrantLock比較
1.區別:
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
總結:ReentrantLock相比synchronized,增加了一些高級的功能。但也有一定缺陷。
在ReentrantLock類中定義了很多方法,比如:
getHoldCount():查詢當前線程保持此鎖定的個數。
getQueueLength(): 返回正等待獲取鎖定的線程估計數。
getWaitQueueLength(): 返回等待與此鎖相關的給定條件Condition的線程估計數。
hasQueuedThread(Thread thread): 查詢指定線程正在等待獲取此鎖定。
hasQueuedThreads(): 獲取是否有線程在等待此鎖定。
hasWaiters():查詢是否有線程在等待與此鎖定有關的Condition條件。
isFair():判斷是否為公平鎖。
isHeldByCurrentThread(): 當前線程是否保持此鎖定。
isLocked(): 查詢此鎖定是否有任意線程保持。
lockInterruptibly(): 當前線程未被中斷,則獲取鎖定。
tryLock():僅在調用時鎖定未被另一個線程保持的情況下獲取該鎖定。
tryLock(long timeout,TimeUnit unit): 如果鎖定在給定等待時間內沒有被另外一個線程保持,且當前線程未被中斷,則獲取該鎖定。
性能:
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時ReentrantLock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
在JDK1.5中,synchronized是性能低效的。因為這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的並發性帶來了很大的壓力。相比之下使用Java提供的ReentrankLock對象,性能更高一些。到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化余地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。
特別是你去看看ConcurrentHashMap的鎖實現,在jdk1.7使用的就是segment分段鎖(ReentrantLock實現),但到了jdk1.8就拋棄了segment分段鎖,直接使用synchronized+CAS實現鎖機制。
多線程編程之synchronized和Lock