本次内容主要讲等待/通知机制以及用等待/通知机制手写一个数据库连接池。

1、为什么线程之间需要协作

  线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(What)和“怎么做”(How)。简单的办法是让消费者线程不断地循环检查变量是否符合预期,在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。这样进行线程之间的协作却存在如下2个问题:

(1)难以确保及时性。

(2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

那么有没有什么办法可以解决以上2个问题呢?此时等待/通知机制毫不客气的站出来说,都让开,交给我,我能行!  

2、等待/通知机制

2.1 等待/通知机制介绍

等待/通知机制是指一个线程A调用了对象obejct的wait()方法进入等待状态,而另一个线程B调用了对象obejct的notify()或者notifyAll()方法,线程A收到通知后从对象obejct的wait()方法返回,进而执行后续操作。上述两个线程通过对象obejct来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify():

通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁。哪个线程能得到通知是随机的,不能指定。

notifyAll():

通知所有等待在该对象上的线程,这些线程会去竞争对象锁,得到锁的某一个线程可以继续执行wait()后的逻辑。

wait():

调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回。需要注意,调用wait()方法后,会释放对象的锁。

wait(long):

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。

wait (long,int):

对于超时时间更细粒度的控制,可以达到纳秒。

2.1 等待/通知机制使用的标准范式

等待方遵循如下原则:

(1)获取对象的锁。

(2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。

(3)条件满足则执行对应的逻辑。

用一段伪代码表示:

  synchronized (对象) {
while (条件不满足) {
对象.wait();
}
对应逻辑处理
}

通知方遵循如下原则:

(1)获得对象的锁。

(2)改变条件。

(3)通知所有等待在对象上的线程。

(4)通知方法放在同步代码块的最后一行。

用一段伪代码表示:

    synchronized (对象) {
改变条件
对象.notifyAll();
}

  在调用wait()notify()和notifyAll()方法之前,线程必须要获得该对象的对象锁,即只能在同步方法或同步块中调用wait()方法、notify()和notifyAll()方法。调用wait()方法后,当前线程释放锁, 执行notify()和notifyAll()方法的线程退出synchronized代码块的时候,假设是执行的notifyAll(),会唤醒所有处于等待的线程,这些线程会去竞争对象锁。如果其中一个线程A获得了该对象锁,线程A就会继续往下执行,其余被唤醒的线程处于阻塞状态。在线程A退出synchronized代码块释放锁后,其余已经被唤醒的处于阻塞状态的线程将会继续竞争该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

2.2 notify()和notifyAll()应该用谁

  尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

2.3 手写一个数据库连接池

  我们在使用数据库连接池的时候,当某一个线程超过配置的最大等待时长还没有拿到连接时,就会报出异常。我们使用等待/通知机制来模拟一个数据库连接池。分别定义连接类、连接池实现类和测试类。

连接类:

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor; public class MySqlConnection implements Connection {
public static final Connection createConnection(){
return new MySqlConnection();
}
//todo 其余接口使用默认实现,这里就不一一给出。
}

连接池实现类:

import java.sql.Connection;
import java.util.LinkedList; public class MyConnectionPool {
/**装连接的容器*/
private static LinkedList<Connection> pool = new LinkedList<>(); /**
* 初始化连接池
* @param poolSize
*/
public MyConnectionPool(int poolSize){
if(poolSize > 0){
for (int i = 0; i < poolSize; i++) {
pool.add(MySqlConnection.createConnection());
}
}
} /**
* 释放一个连接
* @param connection
*/
public void releaseConnection(Connection connection){
if(connection != null){
synchronized (pool){
pool.add(connection);
pool.notifyAll();
}
}
} /**
* 获取一个连接
* @param millions 超时时间
* @return
*/
public Connection getConnection(long millions){
synchronized (pool){
if(millions <= 0){
while (pool.isEmpty()){
try {
pool.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
return pool.removeFirst();
}
else {
//计算超时时刻
long overTime = System.currentTimeMillis() + millions;
//剩余等待时长
long remaining = millions;
//当剩余等待时间大于0并且连接池为空,就等待
while (remaining > 0 && pool.isEmpty()){
try {
pool.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
//被唤醒后重新计算剩余等待时长
remaining = overTime - System.currentTimeMillis();
}
Connection result = null;
if(!pool.isEmpty()){
result = pool.removeFirst();
}
return result;
}
}
}
}

测试类:

import java.sql.Connection;
import java.sql.Statement;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger; public class MyConnectionPoolTest {
/**初始化10个数据库连接*/
static MyConnectionPool pool = new MyConnectionPool(10); /**等待时长为1秒*/
static long maxWait = 1000; /**获取连接的线程数量*/
static int threadNumber = 500; /**所有获取连接线程执行完成后再执行main线程*/
static CountDownLatch mainCountDownLatch = new CountDownLatch(threadNumber); /**保证所有获取连接线程同时执行*/
static CountDownLatch workCountDownLatch = new CountDownLatch(threadNumber); static class Worker implements Runnable{
AtomicInteger success;
AtomicInteger fail; public Worker(AtomicInteger success, AtomicInteger fail){
this.success = success;
this.fail = fail;
} @Override
public void run() {
try {
workCountDownLatch.await();
Connection connection = pool.getConnection(maxWait);
if(connection != null){
try{
Statement statement = connection.createStatement();
Thread.sleep(30);//休眠30毫秒模拟实际业务
connection.commit();
}finally {
pool.releaseConnection(connection);
success.getAndAdd(1);
}
}
else {
fail.getAndAdd(1);
System.out.println(Thread.currentThread().getName() + "等待超时,没有拿到连接!");
}
}catch (Exception e){
e.printStackTrace();
}
mainCountDownLatch.countDown();
}
} public static void main(String[] args) {
AtomicInteger success = new AtomicInteger(0);//记录成功次数
AtomicInteger fail = new AtomicInteger(0);//记录失败次数
for (int i = 0; i < threadNumber; i++) {
new Thread(new Worker(success, fail)).start();
workCountDownLatch.countDown();
}
try {
mainCountDownLatch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("总共尝试获取连接次数:" + threadNumber);
System.out.println("成功次数:" + success.get());
System.out.println("失败次数:" + fail.get());
}
}

运行程序,从输出结果可以看出,通过通知/等待超时模式成功的实现了一个简易的数据库连接池。

2.4 常见面试题

调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?

答:yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

调用wait()方法后,会释放当前线程持有的锁,而且当前线程被唤醒后,会重新去竞争锁,得到锁到后才会执行wait()方法后面的代码。

调用notify()系列方法后,对锁无影响,线程只有在synchronized同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是synchronized同步代码的最后一行。

线程的并发工具类在下一篇文章中介绍,在阅读过程中如发现描述有误,请指出,谢谢。

java线程间的协作的更多相关文章

  1. Java并发之线程间的协作

    上篇文章我们介绍了synchronized关键字,使用它可以有效的解决我们多线程所带来的一些常见问题.例如:竞态条件,内存可见性等.并且,我们也说明了该关键字主要是一个加锁和释放锁的集成,所有为能获得 ...

  2. Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  3. 【转】Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线 ...

  4. java并发之线程间通信协作

    在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界 ...

  5. java并发编程(一)线程状态 & 线程中断 & 线程间的协作

    参考文章: Java线程的5种状态及切换:http://blog.csdn.net/pange1991/article/details/53860651 线程的5种状态: 1. 新建(NEW):新创建 ...

  6. 线程间的协作(wait/notify/sleep/yield/join)(五)

    一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线 ...

  7. Java 线程间通讯(共享变量方式)

    Java线程间通讯,最常用的方式便是共享变量方式,多个线程共享一个静态变量就可以实现在线程间通讯,但是这需要注意的就是线程同步问题. 一.没考虑线程同步: package com.wyf; publi ...

  8. Java线程间通信-回调的实现方式

    Java线程间通信-回调的实现方式   Java线程间通信是非常复杂的问题的.线程间通信问题本质上是如何将与线程相关的变量或者对象传递给别的线程,从而实现交互.   比如举一个简单例子,有一个多线程的 ...

  9. 说说Java线程间通信

    序言 正文 [一] Java线程间如何通信? 线程间通信的目标是使线程间能够互相发送信号,包括如下几种方式: 1.通过共享对象通信 线程间发送信号的一个简单方式是在共享对象的变量里设置信号值:线程A在 ...

随机推荐

  1. hibernate中lazy的使用

    lazy,延迟加载 Lazy的有效期:只有在session打开的时候才有效:session关闭后lazy就没效了. lazy策略可以用在: * <class>标签上:可以取值true/fa ...

  2. Hadoop_在linux中安装hadopp出现的问题

    问题 sudo: no valid sudoers sources found, quitting 网络解决方法 链接:sudo: no valid sudoers sources found, qu ...

  3. CAS 5.3.x 相关信息

    CAS 5.3.x 相关信息 单点登录系统 学习网站: https://www.apereo.org/projects/cas 官方网站 https://github.com/apereo/cas-o ...

  4. 吴裕雄--天生自然python学习笔记:python OpenCV 基本绘图

    Open CV 提供了绘制直线.圆形.矩形等基本绘 图的功能 . Open CV 画直线的语法为: 在画布上添加文字的语法为 : 用 Open CV 绘制基本图形 以 OpenCV 基本绘图绘制各种图 ...

  5. JsonPath入门教程

    有时候需要从json里面提取相关数据,必须得用到如何提取信息的知识,下面来写一下 语法格式 JsonPath 描述 $ 根节点 @ 当前节点 .or[] 子节点 .. 选择所有符合条件的节点 * 所有 ...

  6. 视觉SLAM算法框架解析(3) SVO

    版权声明:本文为博主原创文章,未经博主允许不得转载. SVO(Semi-direct Visual Odometry)[1]顾名思义是一套视觉里程计(VO)算法.相比于ORB-SLAM,它省去了回环检 ...

  7. DB2数据库多行一列转换成 一行一列

    在db2中遇到多行一列转为一行一列的需求时,用db2函数 LISTAGG可以实现该功能.语法如下: SELECT   [分组的字段 ] , LISTAGG([需要聚合的字段名], ',')   FRO ...

  8. 关于js中的比较时遇到的坑

    关于JavaScript中比较遇到的坑 当你的要比较数字的大小但是你的数字确是字符串时,就会出错比如说: console.log('5' > '6') // fasle consloe.log( ...

  9. 控制webbrowser滚动到指定位置

    在构造函数中添加事件: webBrowser.DocumentCompleted+=new WebBrowserDocumentCompletedEventHandler(webBrowser_Doc ...

  10. jenkins发布项目到远程主机上,配置linux使用SSH免密码登录

    一.首先要配置两台linux如何使用SSH免密码登录,这样脚本执行scp命令以及远程执行脚本都不需要输入密码: A为本地主机(即用于控制其他主机的机器,jenkins服务器) ; B为远程主机(即被控 ...