Lock鎖與生產者消費者問題
- 傳統Synchronized鎖
- Lock鎖
- Synchronized和lock鎖的區別
- 傳統的生產者和消費者問題
- Lock版的生產者和消費者問題
- Condition實作精準通知喚醒
傳統Synchronized鎖
實作一個基本的售票例子:
/*
真正的多執行緒開發,公司中的開發,降低耦合性
執行緒就是一個單獨的資源類,沒有任何附屬的操作
1.屬性,方法
* */
public class SaleTicketDemo1 {
public static void main(String[] args) {
//并發,多個執行緒操作同一個資源類,把資源類丟入執行緒
Ticket ticket=new Ticket();
//Runnable借口是一個FunationalInterface函式式介面,介面可以new,jdk1.8以后,lamda運算式()->{代碼}
new Thread(()->{
for(int i=0;i<60;i++){
ticket.sale();
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<60;i++){
ticket.sale();
}
}
},"B").start();
new Thread(()->{
for(int i=0;i<60;i++){
ticket.sale();
}
},"C").start();
}
}
//資源類 OOP
class Ticket{
//屬性,方法
private int number=50;
//賣票的方式
//synchronized本質:佇列,所
public synchronized void sale(){
if(number>0){
System.out.println(Thread.currentThread().getName()+"賣出了"+(number--)+"票,剩余"+number);
}
}
}
注意,這里面用到了lambda運算式,lambda運算式詳細描述見Java基礎-Lambda運算式

這是使用傳統的synchronized實作并發,synchronized的本質就是佇列,鎖,就好比食堂排隊,如果沒有排隊,就會很亂,只有給一個人服務完成了,另一個人才能接收到服務,
Lock鎖
之前已經說道,JVM提供了synchronized關鍵字來實作對變數的同步訪問以及用wait和notify來實作執行緒間通信,在jdk1.5以后,JAVA提供了Lock類來實作和synchronized一樣的功能,并且還提供了Condition來顯示執行緒間通信,
Lock類是Java類來提供的功能,豐富的api使得Lock類的同步功能比synchronized的同步更強大,
在java.util. Concurrent包中,里面有3個介面,Condition,lock(標準鎖),ReadWriteLock鎖(讀寫鎖)
Lock實作提供比使用synchronized方法和陳述句可以獲得的更廣泛的鎖定操作, 它們允許更靈活的結構化,可能具有完全不同的屬性,并且可以支持多個相關聯的物件Condition,
Lock l = ...; l.lock();
try { // access the resource protected by this lock }
finally
{ l.unlock(); }
lock()表示加鎖,unlock()表示解鎖
JDK官方檔案中解釋

所有已知實作類:
ReentrantLock可重入鎖
ReentrantReadWriteLock.ReadLock 讀鎖
ReentrantReadWriteLock.writeLock寫鎖
先說ReentrantLock實作類:
ReentrantLock底層原始碼建構式

公平鎖:十分公平,可以先來后到,但是問題如果一個3s和一個3h的行程到達,3h先,那么3s等3h,實際上也不利,
非公平鎖:十分不公平,可以插隊(默認)
之后,我們會具體解釋,
怎么用,用之前加鎖,用之后解鎖
//lock鎖三部曲
//1.new ReentranLock();構造
//2.Lock.lock();加鎖
//3.finally();解鎖
public class SaleTicketDemo2 {
public static void main(String[] args) {
//并發,多個執行緒操作同一個資源類,把資源類丟入執行緒
Ticket ticket=new Ticket();
//Runnable借口是一個FunationalInterface函式式介面,介面可以new,jdk1.8以后,lamda運算式()->{代碼}
new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"A").start();
new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"B").start();
new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"C").start();
}
}
//資源類 OOP
//lock鎖三部曲
//1.new ReentranLock();
//2.Lock.lock();加鎖
//3.finally();解鎖
class Ticket2{
//屬性,方法
private int number=50;
//賣票的方式
//synchronized本質:佇列,所
Lock lock=new ReentrantLock();
public void sale(){
lock.lock();
try {
//業務代碼
if(number>0){
System.out.println(Thread.currentThread().getName()+"賣出了"+(number--)+"票,剩余"+number);
}
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
}
Synchronized和lock鎖的區別
1.synchronized是內置的java關鍵字,lock是一個Java類
2.synchronized無法判斷獲取鎖的狀態,lock可以判斷是否獲取到了鎖
3.synchronized會自動釋放鎖(a–),lock必須要手動釋放鎖!如果不釋放鎖,會導致死鎖
4.Synchronized執行緒1(獲得鎖,阻塞),執行緒2(等待,傻傻的等)
lock.tryLock()嘗試獲取鎖,不一定會一直等待下去
5.Synchronized可重入鎖,不可以中斷的,非公平鎖,Lock,可重入鎖,可以判斷鎖,公平與非公平可以自己設定(可以自己設定)
6.synchronized適合少量的代碼同步問題,lock鎖適合鎖大量的同步代碼
synchornized鎖物件和同步代碼塊方法
傳統的生產者和消費者問題
傳統的生產者和消費者是基于Object類的wait、notify方法和synchronized關鍵字來實作的,
在面試的時候,手寫生產者消費者代碼是很常見的事情,
面試筆試經典問題:
單例模式+排序演算法+生產者消費者+死鎖
生產者消費者問題synchronized版
執行緒之間的通信問題:生產者和消費者問題 等待喚醒,通知喚醒
執行緒交替執行 A B 操作同一個變數number=0
A num+1
B num-1
注意:加鎖的方法中,執行的思路是判斷等待+業務+通知
package testConcurrent;
/*
執行緒之間的通信問題:生產者和消費者問題 等待喚醒,通知喚醒
執行緒交替執行 A B 操作同一個變數number=0
A num+1
B num-1
* */
public class A {
public static void main(String[] args) {
Data data =new Data();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"B").start();
}
}
//判斷等待+業務+通知
class Data{ //數字,資源類
private int number=0;
//+1,多執行緒的情況下一定要加鎖,
public synchronized void increment() throws InterruptedException{
//判斷是否需要等待,如果不需要,就需要干活進行業務操作
if(number!=0){ //等于1的時候,需要等待,緩沖區只有1個空位置
//等待操作
this.wait();
}
number++; //進行業務操作
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他執行緒,我+1完畢了
this.notify();
}
//-1
public synchronized void decrement() throws InterruptedException{
if(number==0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他執行緒,我-1完畢了
this.notify();
}
}

如圖,基本可以實作所要求的功能,但是這樣還會出現問題,如果此時我再加上了兩個執行緒,則
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"D").start();

這里結果中出現了2,輸出結果出現了問題,為什么呢?
為什么if判斷會出現問題:
if判斷只判斷一次,因為if判斷了之后,就已經進入了代碼的等待那一行,這時,在wait下的執行緒可能有多個,甚至包括生產者和消費者,有可能某個生產者執行完了之后,喚醒的是另一個生產者,
在我們的官方檔案中就給出了解釋
public final void wait(long timeout)
throws InterruptedException
導致當前執行緒等待,直到另一個執行緒呼叫此物件的notify()方法或notifyAll()方法,或指定的時間已過,
執行緒也可以喚醒,而不會被通知,中斷或超時,即所謂的虛假喚醒 , 雖然這在實踐中很少會發生,但應用程式必須通過測驗應該使執行緒被喚醒的條件來防范,并且如果條件不滿足則繼續等待, 換句話說,等待應該總是出現在回圈中,就像這樣:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
注意點:防止虛假喚醒問題,
我們代碼中用的是if判斷,而應該用while判斷
package testConcurrent;
/*
執行緒之間的通信問題:生產者和消費者問題 等待喚醒,通知喚醒
執行緒交替執行 A B 操作同一個變數number=0
A num+1
B num-1
* */
public class A {
public static void main(String[] args) {
Data data =new Data();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.decrement();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"D").start();
}
}
//判斷等待+業務+通知
class Data{ //數字,資源類
private int number=0;
//+1
public synchronized void increment() throws InterruptedException{
//判斷是否需要等待,如果不需要,就需要干活進行業務操作
while(number!=0){ //等于1的時候,需要等待,緩沖區只有1個空位置
//等待操作
this.wait();
}
number++; //進行業務操作
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他執行緒,我+1完畢了
this.notify();
}
//-1
public synchronized void decrement() throws InterruptedException{
while(number==0){
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他執行緒,我-1完畢了
this.notify();
}
}
Lock版的生產者和消費者問題
在synchronized版本中,我們使用了wait和notify來實作執行緒之間的同步
在lock中,
此時synchronized被lock替換了,那么wait和notify用什么來替換呢?
我們在官方檔案java.util.concurrent.locks 中,找到Lock類,然后在底部找到了
Condition newCondition()
回傳一個新Condition系結到該實體Lock實體,
在等待條件之前,鎖必須由當前執行緒保持, 呼叫Condition.await()將在等待之前將原子釋放鎖,并在等待回傳之前重新獲取鎖,
然后我們再來了解Condition類
Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的物件,以便通過將這些物件與任意 Lock 實作組合使用,為每個物件提供多個等待 set(wait-set),其中,Lock 替代了 synchronized 方法和陳述句的使用,Condition 替代了 Object 監視器方法的使用,
一個Condition實體本質上系結到一個鎖, 要獲得特定Condition實體的Condition實體,請使用其newCondition()方法,

我們可以看到,使用的時候new一個Condition物件,然后用await替代wait,signal替換notify
代碼實作
//判斷等待+業務+通知
class Data2{ //數字,資源類
private int number=0;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
//+1
public void increment() throws InterruptedException{
try {
lock.lock();
//業務代碼
while(number!=0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll(); //通知
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException{
try {
lock.lock();
//業務代碼
while(number!=1){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll(); //通知
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
}
注意:主函式部分于最上面的代碼一樣,

這時候雖然說是正確的,但是它是一個隨機分布的狀態,現在我們希望它有序執行,即A執行完了執行B,B執行C,C完了執行D,即精準通知,
Condition實作精準通知喚醒
Condition實作精準的通知和喚醒
我們構造三個執行緒,要求A執行完了執行B,B執行完了執行C,C執行完了執行D.
代碼思想:
//加多個監視器,通過監視器來判斷喚醒的是哪一個人
//設定多個同步監視器,每個監視器監視一個執行緒
//實體:生產線,下單->支付->交易->物流
package testConcurrent;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
A執行完呼叫B,B執行完呼叫C,C執行完呼叫A
* */
public class C {
public static void main(String[] args) {
Data3 data=new Data3();
new Thread(()->{
for(int i=0;i<10;i++){
data.printA();
}
},"A").start();
new Thread(()->{
for(int i=0;i<10;i++){
data.printB();
}
},"B").start();
new Thread(()->{
for(int i=0;i<10;i++){
data.printC();
}
},"C").start();
}
}
class Data3{ //資源類
private Lock lock=new ReentrantLock();
//加多個監視器,通過監視器來判斷喚醒的是哪一個人
//設定多個同步監視器,每個監視器監視一個執行緒
//實體:生產線,下單->支付->交易->物流
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
private int number=1; //1A 2B 3C
public void printA(){
lock.lock();
try {
//業務,判斷->執行->通知
while(number!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
//喚醒,喚醒指定的人,B
number=2; //精準喚醒
condition2.signal();
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
//業務,判斷->執行->通知
while(number!=2){
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
//喚醒,喚醒指定的人,C
number=3; //精準喚醒
condition3.signal();
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
//業務,判斷->執行->通知
while(number!=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCCC");
//喚醒,喚醒指定的人,A
number=1; //精準喚醒
condition1.signal();
} catch (Exception e) {
// TODO: handle exception
}finally{
lock.unlock();
}
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/243568.html
標籤:java
上一篇:switch分支陳述句
下一篇:Java 反射編程(上)
