一、等待與通知機制、等待超時機制
前言
本文主要是講述等待與通知機制和等待與超時機制,三者都是利用wait(),notify(),notifyAll()的使用來實現的。java中的這三個併發屬性是屬於對象的。所以各個線程在操作時候需要對對象的實例方法進行加鎖,進而獲得執行權。所以說是線程來對外方對象來進行加鎖,進而改變條件,通知其他等待在該對象上的線程。舉例實現等待與通知機制-數據庫連接池
1.等待與通知的標準範式
1)等待方
a、獲取對象的鎖
b、循環裏判斷是否滿足條件,不滿足則調用wait()方法
c、條件滿足則執行業務邏輯
2)通知方
a、獲取對象的鎖
b、改變條件
c、通知所有等待在對象上的線程
2.通知:notify和notifyAll該用那個
注視:我們現在做一個例子,設計一個快遞類,有兩個屬性,公里數和城市。現在使用等待與通知機制,當城市變化或者公里數變化時,我們做一些業務動作。
package cn.enjoy.controller.thread;
/**
* @author:wangle
* @description:測試notify和notifyall區別類
* @version:V1.0
* @date:2020-03-14 21:27
**/
public class NotifyAndNotifyAll {
public final static String city = "shanghai";
private int km ; /** 快遞運輸的里程**/
private String site; /** 快遞當前所在城市**/
public NotifyAndNotifyAll(){
}
public NotifyAndNotifyAll(int km,String city){
this.km = km;
this.site = city;
}
public synchronized void changeKm(){
this.km=101;
notifyAll();
}
public synchronized void changeSite(){
this.site="beijing";
notifyAll();
}
public synchronized void waitSite(){
while(city.equals(this.site)){
try {
System.out.println(Thread.currentThread().getName()+" 正在等待城市變化");
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("城市已經從:"+city+"到達了:"+this.site);
}
public synchronized void waitKm(){
while(this.km <= 100){
try {
System.out.println(Thread.currentThread().getName()+" 正在等待公里數變化");
wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("快遞已經距離出發點:"+this.km+"千米");
}
}
package cn.enjoy.controller.thread;
/**
* @author:wangle
* @description:測試
* @version:V1.0
* @date:2020-03-14 21:48
**/
public class TestNotifyAndNotifyAll {
private static NotifyAndNotifyAll notifyAndNotifyAll = new NotifyAndNotifyAll(0,NotifyAndNotifyAll.city);
private static class CheckKm extends Thread{
@Override
public void run(){
notifyAndNotifyAll.waitKm();
}
}
private static class CheckSite extends Thread{
@Override
public void run(){
notifyAndNotifyAll.waitSite();
}
}
public static void main(String[] args)throws InterruptedException{
for(int i=0;i<3;++i){
new CheckSite().start();
}
for(int i=0;i<3;++i){
new CheckKm().start();
}
Thread.sleep(1000);
notifyAndNotifyAll.changeKm();
}
}
使用notifyAll的結果
可以看到,當我們在改變了公里數,通知了所有的wait在該對象上的線程,並且做出了公里數改變了相應的業務。
使用notify的結果
可以看到,我們在改變了條件,使用nitify通知時,隨機選中了一個等待在該對象上的線程(JVM會在等待隊列中取第一個線程進行喚醒),恰巧選中了等待城市變化的線程,但是我們明明改變的是公里數,並且通知。這就是notify,只會從等待中的線程隨機選取一個通知,其他的則不會收到信號。
3、等待超時模式
1)等待超時模式範式
假設需要等待的時間爲T,當前時間加上T以後超時
long overtime = now + T;
long remain = T;
while(result不滿足條件&&remain>0){
wait(remain);
remain = overtime-now;
}
return result;
2)實現一個數據庫連接池
package cn.enjoy.controller.thread.DBPOLL;
import java.sql.Connection;
import java.util.LinkedList;
/**
* @author:wangle
* @description:數據庫連接池
* @version:V1.0
* @date:2020-03-15 16:30
**/
public class DBPoll {
/**連接池大小**/
private static LinkedList<Connection> poll = new LinkedList<>();
/**初始化連接池**/
public DBPoll(int initSize){
if(initSize>0){
for(int i =0;i<initSize;++i){
poll.addLast(SqlConnectImpl.fetchConnection());
}
}
}
/**
* @author wangle25
* @description 獲取連接
* @date 16:48 2020-03-15
* @param time:超時時間
* @return java.sql.Connection
**/
public Connection getConnection(long time)throws InterruptedException{
synchronized (poll){
//永遠不超時
if(time<=0){
while(poll.isEmpty()){
poll.wait();
}
return poll.removeFirst();
}else{
long overTime = System.currentTimeMillis()+time;
long remain = time;
while(poll.isEmpty()&&remain>=0){
poll.wait(remain);
remain = overTime-System.currentTimeMillis();
}
Connection result = null;
if(!poll.isEmpty()){
result = poll.removeFirst();
}
return result;
}
}
}
/**
* @author wangle25
* @description 釋放鏈接
* @date 16:58 2020-03-15
* @param connection
* @return void
**/
public void releaseConnection(Connection connection){
synchronized (poll){
poll.addLast(connection);
poll.notifyAll();
}
}
}
package cn.enjoy.controller.thread.DBPOLL;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author:wangle
* @description:測試數據庫連接
* @version:V1.0
* @date:2020-03-15 16:59
**/
public class DBPollTest {
static DBPoll dBPoll = new DBPoll(10);
static CountDownLatch end;
public static void main(String[] arg)throws Exception{
int threadCount = 50;
end = new CountDownLatch(threadCount);
int workCount = 20;
AtomicInteger got = new AtomicInteger();
AtomicInteger notGot = new AtomicInteger();
for(int i=0;i<threadCount;i++){
Thread thread = new Thread(new Worker(workCount,got,notGot));
thread.start();
}
end.await();
System.out.println("總共嘗試了:"+threadCount*workCount+"次");
System.out.println("獲取到次數:"+got);
System.out.println("未獲取到次數:"+notGot);
}
static class Worker implements Runnable{
int count;
AtomicInteger got;
AtomicInteger notGot;
public Worker( int count,AtomicInteger got,AtomicInteger notGot){
this.count = count;
this.got=got;
this.notGot=notGot;
}
@Override
public void run(){
while(count>0){
try {
Connection connection = dBPoll.getConnection(1000L);
if(null != connection){
try{
connection.createStatement();
connection.commit();
}finally {
dBPoll.releaseConnection(connection);
got.incrementAndGet();
}
}
notGot.incrementAndGet();
}catch (Exception e){
}finally {
count--;
}
}
end.countDown();
}
}
}
結果
闡述:啓動了50個線程、每個線程都獲取數據庫連接20次,有877次成功,123次失敗。以上就是利用等待與通知機制實現的數據庫連接池。
二、join方法
1、作用
可以控制線程執行的順序,例如,線程A執行了線程B的join方法,則線程A需要等待線程B執行完成之後才能繼續執行。下面用一段代碼來驗證
package cn.enjoy.controller.thread.DBPOLL.join方法;
import cn.enjoy.controller.thread.DBPOLL.SleepTools;
/**
* @author:wangle
* @description:使用join方法對線程進行串行化
* @version:V1.0
* @date:2020-03-19 15:28
**/
public class UseJoin {
static class JumpQueue implements Runnable{
private Thread thread;
public JumpQueue(Thread thread){
this.thread = thread;
}
@Override
public void run(){
try {
thread.join();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"run over");
}
}
public static void main(String[] args){
//取到當前主線程
Thread previous = Thread.currentThread();
for(int i=0;i<10;i++){
Thread thread = new Thread(new JumpQueue(previous),String.valueOf(i));
System.out.println(previous.getName()+"插入到了"+thread.getName()+"前面");
thread.start();
previous = thread;
}
SleepTools.second(2);
System.out.println(Thread.currentThread().getName()+"run over");
}
}
闡述:我們循環起了10個線程,當i=0的時候,主線程插到0線程的前面,當i=1,0線程插入到1線程前,依次類推。線程最終的執行順序應該是 main->0->1->2->3->4->5->6->7->8->9
二、yield,wait,sleep,notify對鎖的影響
yield和sleep後是不釋放鎖的
wait方法調用後會釋放鎖
notify方法調用後不會釋放鎖,等待被同步的代碼塊執行完成之後纔會將鎖釋放,所以一般notify和notifyall再代碼同步塊的最後使用比較合理