JUC----等待通知范式和等待超时实现线程池

前言

这篇文章主要是以Object类的wait, notify, notifyAll 为主体方法,描述等待通知范式是什么?如何实现的?还有就是通过等待超时的方法实现一个线程池。

1.等待通知的标准范式

要了解该范式,我们得先从3个方法说起。

  • wait() : 对象调用wait方法之前需要先获取锁。调用方法之后,该锁的标志位将会去除,并进入对象的等待池中。(只有等待notify或notifyAll 方法唤醒后才能重新获取锁)。该方法还有相关的重载方法 wait(long timeout), 这个表示当我们休眠了timeout的时间会被重新唤醒,不过在休眠时间内仍然可以被notify 或notifyAll方法唤醒。
  • notify() 或notifyAll(): 使用该方法之前必须先获取锁,该方法调用之后,本身不会去释放锁,而是伴随着同步代码块的释放而释放(所以该方法建议放在同步代码块的最后)。
  • notify() 和 notifyAll() 的异同:两个方法都能唤醒在该对象等待的线程,不过notify() 只能随机唤醒一个,而notufyAll() 能唤醒全部等待的线程,在使用的过程中,一般建议使用notifyAll(), 因为使用notify() 容易造成信号丢失(下面会通过一个实例说明)。

等待通知的标准范式
等待方:
1.获取对象的锁
2.循环判断条件是否满足,不满足则调用wait方法
3.条件满足则执行业务逻辑
通知方:
1.获取对象的锁
2.改变条件
3.通知所有等待在对象的线程

2. 等待通知范式的实例

业务需求:我们现在有一个快递类,它有里程数和地点这两个变量。有两个方法来表示里程数和地点的等待方;还有另外两个方法来表示这两个变量的通知方。(方法上刚好满足上面的范式)
Express.class

package 等待和通知范式;

public class Express {

    public final static String CITY = "ShangHai";
    private int km;   //快递运输的里程数
    private String site; //快递到达的地点

    public Express() {
    }

    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }
    //通知方
    //变化公里数,然后通知处于wait状态需要处理公里数线程进行业务逻辑
    public synchronized void changeKm(){
        this.km = 101;
        notifyAll();
    }
    //通知方
    //变化地点,然后通知处于wait状态需要处理地点线程进行业务逻辑
    public synchronized void changeSite(){
        this.site = "BeiJin";
        notifyAll();
    }
    //等待方
    public synchronized void waitKm(){
        while(this.km <= 100){
            try {
                wait();
                System.out.println("check km thread["+
                        Thread.currentThread().getId()+"]"+" is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the km is "+this.km+", I will change db.");

    }
     //等待方
    public synchronized void waitSite(){
        while(this.site.equals(CITY)){
            try {
                wait();
                System.out.println("check site thread["+
                        Thread.currentThread().getId()+"]"+" is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the site is "+this.site+", I will call user.");
    }
}

测试类:让3个等待里程数变化和3个等待地点变化的线程运行,并在主方法中调用相关的通知方法,观察控制台的输出。
TestWN.class

package 等待和通知范式;

//  测试 wait/notify/notifyAll
public class TestWN {
   private static Express express = new Express(0, Express.CITY);

   //检查里程数据变化的线程,不满足一直等待
   private static class CheckKm extends Thread{
       @Override
       public void run() {
           express.waitKm();
       }
   }
   //检查地点变化的线程,不满足条件,线程一直等待
   private static class CheckSite extends Thread{
       @Override
       public void run() {
           express.waitSite();
       }
   }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {  //3个地点线程
            new CheckSite().start();
        }
        for (int i = 0; i < 3; i++) { //3个里程数的线程
            new CheckKm().start();
        }
        Thread.sleep(1000);
        express.changeKm();
    }
}

这时是调用了notifyAll() 的方法,6个线程都会被唤醒,但是3个地点变化的线程由于没有满足while的判断,会一直处于阻塞状态。
在这里插入图片描述
当我们把上面的notiofyAll() 改为 notify() 时。(即在通知方法上更改)
这时我们可以看到它会随机唤醒一个线程,此时它刚好唤醒的是地点(而我们改变的确是公里数),这时候,地点的线程仍然会处于阻塞状态,而公里变化的线程却没有得到通知,这就是我们所说的信号丢失(所以我们一般建议使用notifyAll)
在这里插入图片描述

3. 等待超时实现一个连接池

我们平时操作数据库时,由于连接的资源十分珍贵,我们经常使用一个连接池来存取数据库的连接。下面使用一种等待超时的方式实现。

相关的伪代码:(类似于等待通知的范式,不过我们此时可以不用一直等待,当remain时间走完还拿不到相应的连接资源时,我们就可以跳出该循环。)

假设等待时长为 T , 当前时间 now + T 以后超时
long overtime = now + T
long remain  =  T;    //等待的持续时间

while(result 不满足条件 && remain > 0){
	wait(remain);
	remain = overtime - now; //等待剩下的持续时间
}

其他逻辑代码

<1> 编写一个实现了数据连接的实例
(这里我只重写了createStatement和commit 方法,还有加上一个拿数据库连接的方法,其余原方法照旧)

public class SqlConnectImpl implements Connection {

    //拿到一个数据库连接
    public static final Connection fetchConnection(){
        return new SqlConnectImpl();
    }
	//模拟生成操作数据库对象的时间
    @Override
    public Statement createStatement() throws SQLException {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
    //模拟提交数据库的时间
     @Override
    public void commit() throws SQLException {
        try {
            Thread.sleep(70);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    .................其余重写方法省略(都是原方法).................

<2> 实现一个数据库连接池
用一个LinkedList来存放连接资源。
初始化数据库连接池

 //数据库池的容器
 private static LinkedList<Connection> pool = new LinkedList<>();

  //初始化数据库连接池
  public DBPool(int initialSize) {
      if(initialSize > 0){
          for (int i = 0; i < initialSize; i++) {
              pool.add(SqlConnectImpl.fetchConnection());
          }
      }
  }

使用等待超时模式实现拿数据库连接
[1] 当我们等待时间小于0时,表示获取资源时,可以无限等待,直到获取资源后返回
[2] 当我们等待的时间大于等于0时,线程池会等待相应的时间,当池子不为空(表示我们有机会获取连接)或等待已超时(表示我们获取时间过长已经无须等待了)都可以跳出循环。
[3] 跳出循环后,尝试获取连接。

  //使用等待超时模式实现拿数据库连接
    public Connection fetchConn(long mills) throws InterruptedException {
        synchronized (pool){
            //若等待时间小于0,表示可用无限等待
            if(mills < 0){
                while(pool.isEmpty()){
                    pool.wait();
                }
                return pool.removeFirst();
            }else{
                long overTime = mills + System.currentTimeMillis();
                long remain = mills;
                while(pool.isEmpty() && remain > 0){
                    pool.wait(remain);
                    remain = overTime - System.currentTimeMillis();
                }
                //跳出循环时,可能是超时也可能是连接池为空
                Connection result = null;
                if(!pool.isEmpty()){
                    result = pool.removeFirst();
                }
                return  result;
            }
        }
    }

返回数据库连接
[1] 在数据库连接池中放入相应的连接
[2] 通知其他等待的线程

 //返回数据库连接
 public void releaseConn (Connection conn){
      if(conn != null){
          synchronized (pool){
              pool.addLast(conn);
              //通知等待拿数据库连接的线程
              pool.notifyAll();
          }
      }
  }

数据库连接池的完整代码

package 数据库连接池;

import java.sql.Connection;
import java.util.LinkedList;

/**
 * @Auther: Gs
 * @Date: 2020/8/10
 * @Description: 数据库连接池
 * @version: 1.0
 */
//实现一个数据库连接池
public class DBPool {

    //数据库池的容器
    private static LinkedList<Connection> pool = new LinkedList<>();

    //初始化数据库连接池
    public DBPool(int initialSize) {
        if(initialSize > 0){
            for (int i = 0; i < initialSize; i++) {
                pool.add(SqlConnectImpl.fetchConnection());
            }
        }
    }

    //使用等待超时模式实现拿数据库连接
    public Connection fetchConn(long mills) throws InterruptedException {
        synchronized (pool){
            //若等待时间小于0,表示可用无限等待
            if(mills < 0){
                while(pool.isEmpty()){
                    pool.wait();
                }
                return pool.removeFirst();
            }else{
                long overTime = mills + System.currentTimeMillis();
                long remain = mills;
                while(pool.isEmpty() && remain > 0){
                    pool.wait(remain);
                    remain = overTime - System.currentTimeMillis();
                }
                //跳出循环时,可能是超时也可能是连接池为空
                Connection result = null;
                if(!pool.isEmpty()){
                    result = pool.removeFirst();
                }
                return  result;
            }
        }
    }

    //返回数据库连接
    public void releaseConn (Connection conn){
        if(conn != null){
            synchronized (pool){
                pool.addLast(conn);
                //通知等待拿数据库连接的线程
                pool.notifyAll();
            }
        }
    }
}

<3> 测试类
我们初始化一个具有10个连接的线程池,此时我们开启50个线程,每个线程分别操作20次数据库。当在规定时间没有拿到连接时,我们视为连接超时的线程。其余获取成功的线程,可以进行相应的操作,最后统计这1000次操作中,有多少次成功,多少次失败。

相关的工作线程
拿到连接的线程,让Got(原子类)自增,并模拟相关的操作后释放连接;在规定时间没有获取连接失败的让notGot (原子类)自增;而end是我们的一个计数器,表示每运行完一个线程就-1。

 //工作线程
  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{
                   //从线程池中获取连接,如果1000ms内无法获取,将会返回null
                   //分别统计获取的数量got和未获取的数量notGot
                   Connection connection = pool.fetchConn(1000);
                   if(connection != null){
                       try{
                           connection.createStatement();
                           connection.commit();
                       }finally {
                           pool.releaseConn(connection);
                           //先自增再拿
                           got.incrementAndGet();
                       }
                   }else{
                       notGot.incrementAndGet();
                       System.out.println(Thread.currentThread().getName()
                                           +"等待超时");
                   }
               }catch (Exception e){

               }finally {
                   count--;
               }
           }
           //一个工作线程走完就 -1
           end.countDown();
       }
   }

初始化具有10个连接资源的线程池,启动50个线程,每个线程操作20次;由于运行时需要一定的时间,我们使用CountDownLatch 让主线程在工作线程运行时处于阻塞状态,直到全部运行完,才放行并统计最终结果。

 static DBPool pool = new DBPool(10);
    //控制器: 控制main线程将会等待所有的worker线程结束后才能继续执行
    static CountDownLatch end;

    public static void main(String[] args) throws InterruptedException {
        //线程数量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        int count = 20; //每个线程操作的次数
        AtomicInteger got = new AtomicInteger(); //计数器: 统计可以拿到连接的线程
        AtomicInteger notGot = new AtomicInteger(); //计数器: 统计没有拿到连接的线程

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot),
                    "work_"+i);
            thread.start();
        }
        end.await();  //main线程在此处等待
        System.out.println("总共尝试了:"+(threadCount * count));
        System.out.println("查到连接的次数:"+got);
        System.out.println("没能连接的次数:"+notGot);

    }

测试类的完整代码

package 数据库连接池;

import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class DBPoolTest {
    static DBPool pool = new DBPool(10);
    //控制器: 控制main线程将会等待所有的worker线程结束后才能继续执行
    static CountDownLatch end;

    public static void main(String[] args) throws InterruptedException {
        //线程数量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        int count = 20; //每个线程操作的次数
        AtomicInteger got = new AtomicInteger(); //计数器: 统计可以拿到连接的线程
        AtomicInteger notGot = new AtomicInteger(); //计数器: 统计没有拿到连接的线程

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot),
                    "work_"+i);
            thread.start();
        }
        end.await();  //main线程在此处等待
        System.out.println("总共尝试了:"+(threadCount * count));
        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{
                    //从线程池中获取连接,如果1000ms内无法获取,将会返回null
                    //分别统计获取的数量got和未获取的数量notGot
                    Connection connection = pool.fetchConn(1000);
                    if(connection != null){
                        try{
                            connection.createStatement();
                            connection.commit();
                        }finally {
                            pool.releaseConn(connection);
                            //先自增再拿
                            got.incrementAndGet();
                        }
                    }else{
                        notGot.incrementAndGet();
                        System.out.println(Thread.currentThread().getName()
                                            +"等待超时");
                    }
                }catch (Exception e){

                }finally {
                    count--;
                }
            }
            //一个工作线程走完就 -1
            end.countDown();
        }
    }
}

控制台输出(它可以统计获取成功和获取失败的连接数,表明我们实现的数据库连接类是正确的。)
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值