环境:
jdk1.8
摘要说明:
上两章我们主要讲了线程共享的相关关键字的使用方法;
本章节主要讲述线程协作的相关概念及一些关键字用法
线程间的协作:假设A线程不满足某个业务条件进行不下去,线程处于等待中;即此时需要等待B线程进行相关操作满足A线程的业务员条件;如果需要A不停的轮询去查询条件是否满足就显得笨重及不高效,此时如果B满足条件后通知A线程停止等待才最为高效快捷,这就是线程间的协作;典型的线程间的协作有生产者-消费者模式、数据库连接池等;
步骤:
1.常用方法
在第一章的时候我们大概讲述了下线程状态的变化及相关方法的作用,在这里再做阐述下各个方法的作用:
wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒(进入“就绪状态”);
wait(long timeout)让当前线程处于“等待(阻塞)状态”,直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量,当前线程被唤醒(进入“就绪状态”)。
notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
synchronized(),wait(),notify() /notifyAll()需保持对象一致性,即wait(),notify() /notifyAll()需在synchronized()同步锁下才可执行,且wait(),notify() /notifyAll()调用的对象必须和synchronized()锁定的对象一致才可
sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态;
join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。
2.wait(),notify() /notifyAll()
为了很好的说明这三个方法的使用我们引入两个模式:
等待和通知的标准范式:
等待方:
- 获取对象的锁;
- 循环里判断条件是否满足,不满足调用wait方法,线程进入等待状态
- 等待唤醒或自动唤醒且条件满足执行业务逻辑
通知方:
- 获取对象的锁;
- 改变条件
- 通知所有等待在对象的线程notify() /notifyAll()
上述就是一个标准的等待和通知模版;唤醒应该尽量使用notifyAll,使用notify因为有可能发生信号丢失的的情况,我们可以通过下述示例来验证:
/**
* @模块名:study_1_thread_basic
* @包名:pers.cc.curriculum1.wait
* @描述:HealthPush.java
* @版本:1.0
* @创建人:cc
* @创建时间:2019年1月30日下午2:59:43
*/
package pers.cc.curriculum1.wait;
/**
* @模块名:study_1_thread_basic
* @包名:pers.cc.curriculum1.wait
* @类名称: HealthPush
* @类描述:【类描述】标准通知方式演示
* @版本:1.0
* @创建人:cc
* @创建时间:2019年1月30日下午2:59:43
*/
public class HealthPush {
public final static int HEIGHTFORMAN = 120;
private static String name;
// 体重
private int weight;
// 身高
private int height;
public HealthPush() {
}
public HealthPush(int weight, int height) {
this.weight = weight;
this.height = height;
}
/**
* 体重变更,唤醒所有等待中线程进
*
* @param weight
*/
public synchronized void changeWeight(int weight) {
this.weight = weight;
notifyAll();
// 其他的业务代码
}
/**
* 身高发生变化,随机唤醒一个线程进行业务处理
*
* @param height
*/
public synchronized void changeHeight(int height) {
this.height = height;
notify();
}
public synchronized void waitWeight() {
while (this.weight <= 70) {
try {
System.out.println("check weight thread["
+ Thread.currentThread().getId() + "] is be wait.");
wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out
.println("the weight is" + this.weight + ",I will change db.");
}
public synchronized void waitHeight() {
while (this.height < HealthPush.HEIGHTFORMAN) {
try {
System.out.println("check height thread["
+ Thread.currentThread().getId() + "] is be wait.");
wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out
.println("the height is" + this.height + ",I will call user.");
}
public static void main(String[] args) throws InterruptedException {
HealthPush healthPush = new HealthPush(35, 100);
for (int i = 0; i < 3; i++) {
// 三个线程监控体重
new Thread(new Runnable() {
@Override
public void run() {
healthPush.waitWeight();
}
}).start();
// 三个线程监控身高
new Thread() {
@Override
public void run() {
healthPush.waitHeight();
}
}.start();
}
Thread.sleep(1000);
// healthPush.changeWeight(80);// 体重发生变化
healthPush.changeHeight(140);// 身高发生变化
}
}
执行notify()则只唤醒一个线程:
注:许多博客说唤醒是按等待顺序唤醒,但api上说的是随机,即使你按序唤醒,唤醒多个时也不能保证能按序获得锁;
check weight thread[10] is be wait.
check weight thread[12] is be wait.
check height thread[11] is be wait.
check height thread[13] is be wait.
check weight thread[14] is be wait.
check height thread[15] is be wait.
check weight thread[10] is be wait.
执行notifyAll()则唤醒全部等待中的线程:
check weight thread[10] is be wait.
check height thread[13] is be wait.
check height thread[11] is be wait.
check weight thread[12] is be wait.
check weight thread[14] is be wait.
check height thread[15] is be wait.
check height thread[15] is be wait.
the weight is80,I will change db.
the weight is80,I will change db.
check height thread[11] is be wait.
check height thread[13] is be wait.
the weight is80,I will change db.
等待超时模式:
伪代码模型如下:
假设 等待时间时长为T,当前时间now+T以后超时
long overtime = now+T;
long remain = T;//等待的持续时间
while(result不满足条件&& remain>0){
wait(remain);
remain = overtime – now;//等待剩下的持续时间
}
return result;
最常见的等待超时模式运用场景就是数据库连接池,获取数据库连接时如果超时返回null或者异常;
下面我们就实行一个等待超时模式的简易数据库连接池;
首先实现一个数据库连接:
package pers.cc.curriculum1.pool;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import pers.cc.tools.SleepTools;
/**
*
* @author cc 假设是一个数据库厂商实现数据库连接
*
*/
public class SqlConnectImpl implements Connection {
/* 拿一个数据库连接 */
public static final Connection fetchConnection() {
return new SqlConnectImpl();
}
@Override
public Statement createStatement() throws SQLException {
SleepTools.ms(1);
return null;
}
@Override
public void commit() throws SQLException {
SleepTools.ms(70);
}
/**********更多见下方源代码***********/
再模拟一个数据库连接池:
package pers.cc.curriculum1.pool;
import java.sql.Connection;
import java.util.LinkedList;
/**
*
* @author cc 模拟一个连接池
*
*/
public class DBPool {
// 数据库池的容器
private static LinkedList < Connection > pool = new LinkedList <>();
/**
* 初始化连接池
*
* @param initalSize
*/
public DBPool(int initalSize) {
if (initalSize > 0) {
for (int i = 0; i < initalSize; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
}
/**
* 获取数据库连接
*
* @param mills
* 超时毫秒数
* @return 在mills时间内还拿不到数据库连接,返回一个null
* @throws InterruptedException
*/
public Connection fetchConn(long mills) throws InterruptedException {
// 先获得同步锁
synchronized (pool) {
// 如果等待时间小于0则无线等待
if (mills < 0) {
while (pool.isEmpty()) {
pool.wait();
}
return pool.removeFirst();
}
else {
long overtime = System.currentTimeMillis() + mills;
long remain = mills;
// 无数据库连接且等待时间大于0则进入等待
while (pool.isEmpty() && remain > 0) {
pool.wait(remain);
// 唤醒后更新可等待时间
remain = overtime - System.currentTimeMillis();
}
// 若无连接则返回null,否则取第一个
Connection result = null;
if (!pool.isEmpty()) {
result = pool.removeFirst();
}
return result;
}
}
}
/**
* 释放数据库连接,并唤醒等待线程
*
* @param conn
*/
public void releaseConn(Connection conn) {
if (conn != null) {
// 唤醒前需先获得锁
synchronized (pool) {
pool.addLast(conn);
pool.notifyAll();
}
}
}
}
进行并发测试:
package pers.cc.curriculum1.pool;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 数据库连接测试
*
* @author cc
*
*/
public class DBpoolTest {
// 定义一个连接池,大小为10
static DBPool pool = new DBPool(10);
// 定义一个发令枪
static CountDownLatch end;
// 定义线程数量
static int threadCount = 50;
// 定义每个线程操作次数
static int count = 50;
// //计数器:统计可以拿到连接的线程
static AtomicInteger got = new AtomicInteger();
// 计数器:统计没有拿到连接的线程
static AtomicInteger notGot = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
// 初始化发令枪
end = new CountDownLatch(DBpoolTest.threadCount);
for (int i = 0; i < DBpoolTest.threadCount; i++) {
Thread thread = new Thread(new Worker(count, got, notGot),
"worker_" + i);
thread.start();
}
// main线程在此处等待所有子线程结束
end.await();
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;
}
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 ex) {
}
finally {
count--;
}
}
// 线程准备完成,发令枪计数减少
end.countDown();
}
}
}
运行结果:
......
worker_0等待超时!
worker_30等待超时!
worker_25等待超时!
worker_34等待超时!
worker_14等待超时!
worker_5等待超时!
总共尝试了: 2500
拿到连接的次数: 2180
没能连接的次数: 320
上述就是一个等待超时模式的示例,注意的点就是唤醒后需要重新计算可等待时间;
3.sleep
sleep上述已阐述,在这里主要强调sleep只是让出cpu的使用权,进入阻塞状态,并不会释放锁;
package pers.cc.curriculum1.sleep;
/**
* sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
*
* @author cc
*
*/
public class SleepTest {
/**
* 线程睡眠不释放锁但让出cpu执行权
*/
public synchronized void sleepMethod() {
System.out.println("sleepMethod start");
try {
Thread.sleep(1000);
System.out.println("sleepMethod end");
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 线程等待,让出锁,等待被唤醒或自动唤醒
*/
public synchronized void waitMethod() {
System.out.println("waitMethod start");
try {
wait(1000);
System.out.println("waitMethod end");
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("测试sleep");
SleepTest test1 = new SleepTest();
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
test1.sleepMethod();
}
}.start();
}
SleepTools.ms(5000);
System.out.println("测试wait");
SleepTest test2 = new SleepTest();
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
test2.waitMethod();
}
}.start();
}
}
}
上述示例的运行结果如下,很好的说明了sleep和wait之间的区别:
测试sleep
sleepMethod start
sleepMethod end
sleepMethod start
sleepMethod end
sleepMethod start
sleepMethod end
测试wait
waitMethod start
waitMethod start
waitMethod start
waitMethod end
waitMethod end
waitMethod end
4.join
join的本质就是将两个并行的线程串联同步运行:
package pers.cc.curriculum1.join;
import pers.cc.tools.SleepTools;
/**
* 进行join测试
*
* @author cc
*
*/
public class JoinTest {
/**
* 进行线程嵌套
*
* @author cc
*
*/
static class JumpQueue implements Runnable {
private Thread thread;// 用来插队的线程
public JumpQueue(Thread thread) {
this.thread = thread;
}
public void run() {
try {
System.out.println(thread.getName() + " will be join before "
+ Thread.currentThread().getName());
thread.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out
.println(Thread.currentThread().getName() + " terminted.");
}
}
public static void main(String[] args) {
Thread previous = Thread.currentThread();// 现在是主线程
for (int i = 0; i < 10; i++) {
// 新建线程并命名为i嵌套在线程previous下
Thread thread = new Thread(new JumpQueue(previous),
String.valueOf(i));
// System.out.println(previous.getName() +
// " jump a queue the thread:"
// + thread.getName());
thread.start();
// 更换主嵌套线程
previous = thread;
}
SleepTools.second(2);// 让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
上述演示的结果如下:
main will be join before 0
0 will be join before 1
1 will be join before 2
2 will be join before 3
3 will be join before 4
4 will be join before 5
5 will be join before 6
6 will be join before 7
8 will be join before 9
7 will be join before 8
main terminate.
0 terminted.
1 terminted.
2 terminted.
3 terminted.
4 terminted.
5 terminted.
6 terminted.
7 terminted.
8 terminted.
9 terminted.
注:join方法还有join(long millis)用法,此用法指的是嵌套线程若在规定时间内没执行完则并行执行;不建议使用
5.yield
yield方法需要注意的是不会释放锁的且不稳定,调度器有可能会忽略该方法,主要用于调试和测试;
package pers.cc.curriculum1.yield;
/**
* 进行yield测试
*
* @author cc
*
*/
public class YieldTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
// 进行线程切换调用
Thread.yield();
}
}
public static void main(String[] args) {
new Thread(new YieldTest(), "first").start();
new Thread(new YieldTest(), "second").start();
}
}
上述代码示例的运行结果如下:
second: 0
first: 0
first: 1
second: 1
first: 2
second: 2
first: 3
second: 3
first: 4
second: 4