线程间的通信和协调、协作
线程间的通信和协调、协作
很多时候,我们都是多线程一起工作的,因为一个线程工作并没有太大的用处。而这些线程间进行通信或者配合完成某项工作,就离不开线程间的通信和协调、协助。
管道输入输出流
Java线程里也有类似于管道的进程间通信机制,用于线程之间的数据传输,而传输的媒介则是内存。
Java中的管道输入/输出流主要包括了如下4种具体实现:
- PipedOutputStream
- PipedInputStream
- PipedReader
- PipedWriter
前两种面向字节,而后两种面向字符。
实例代码:
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
//将键盘的输入,用输出流接受,在实际的业务中,可以将文件流导给输出流
while ((receive = System.in.read()) != -1){
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
//输入流从输出流接收数据,并在控制台显示
//在实际的业务中,可以将输入流直接通过网络通信写出
while ((receive = in.read()) != -1){
System.out.print((char) receive);
}
} catch (IOException ex) {
}
}
}
join方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B剩下的代码。
示例代码
static class Goddess implements Runnable {
private Thread thread;
public Goddess(Thread thread) {
this.thread = thread;
}
public Goddess() {
}
public void run() {
System.out.println("Goddess开始排队打饭.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " Goddess打饭完成.");
}
}
static class GoddessBoyfriend implements Runnable {
public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("GoddessBoyfriend开始排队打饭.....");
System.out.println(Thread.currentThread().getName()
+ " GoddessBoyfriend打饭完成.");
}
}
public static void main(String[] args) throws Exception {
Thread zhuGe = Thread.currentThread();
GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
Thread gbf = new Thread(goddessBoyfriend);
Goddess goddess = new Goddess(gbf);
Thread g = new Thread(goddess);
g.start();
gbf.start();
System.out.println("zhuGe开始排队打饭.....");
g.join();
SleepTools.second(2);//让主线程休眠2秒
System.out.println(zhuGe.getName() + " zhuGe打饭完成.");
}
示例代码执行结果:
synchronized内置锁
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁
对象锁
对象锁是用于对象实例方法,或者一个对象实例上的。
类锁
类锁是用于类的静态方法或者一个类的class对象上的。
类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
volatile,最轻量的通信/同步机制
volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环,而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。
但是volatile不能保证数据在多个线程下同时写时的线程安全,volatile最适用的场景:一个线程写,多个线程读。
等待/通知机制
线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How)。
简单的办法是让消费者线程不断地循环检查变量是否符合预期在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。却存在如下问题:
- 难以确保及时性。
- 难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。
等待/通知机制则可以很好的避免,这种机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待/通知机制方法
notify():
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
notifyAll():
通知所有等待在该对象上的线程。
wait()
调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。
wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒。
等待和通知的标准范式
在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁,执行notify()系列方法的线程退出调用了notifyAll的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
等待方遵循原则
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
- 满足则执行对应的逻辑。
通知方遵循原则
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
notify和notifyAll应该用谁
尽可能用notifyall(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
等待超时模式实现一个连接池
调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
假设等待时间段是T,那么可以推断出在当前时间now+T之后就会超时。
等待持续时间:REMAINING=T。
超时时间:FUTURE=now+T。
public class SqlConnectImpl implements Connection{
/*拿一个数据库连接*/
public static final Connection fetchConnection(){
return new SqlConnectImpl();
}
// TODO 实现Connection方法
}
/**
*类说明:连接池的实现
*/
public class DBPool {
/*容器,存放连接*/
private static LinkedList<Connection> pool = new LinkedList<Connection>();
/*限制了池的大小=20*/
public DBPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
}
/*释放连接,通知其他的等待连接的线程*/
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (pool){
pool.addLast(connection);
//通知其他等待连接的线程
pool.notifyAll();
}
}
}
/*获取*/
// 在mills内无法获取到连接,将会返回null 1S
public Connection fetchConnection(long mills)
throws InterruptedException {
synchronized (pool){
//永不超时
if(mills<=0){
while(pool.isEmpty()){
pool.wait();
}
return pool.removeFirst();
}else{
/*超时时刻*/
long future = System.currentTimeMillis()+mills;
/*等待时长*/
long remaining = mills;
while(pool.isEmpty()&&remaining>0){
pool.wait(remaining);
/*唤醒一次,重新计算等待时长*/
remaining = future-System.currentTimeMillis();
}
Connection connection = null;
if(!pool.isEmpty()){
connection = pool.removeFirst();
}
return connection;
}
}
}
}
public class DBPoolTest {
static DBPool pool = new DBPool(10);
// 控制器:控制main线程将会等待所有Woker结束后才能继续执行
static CountDownLatch end;
public static void main(String[] args) throws Exception {
// 线程数量
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),
"worker_"+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;
}
public void run() {
while (count > 0) {
try {
// 从线程池中获取连接,如果1000ms内无法获取到,将会返回null
// 分别统计连接获取的数量got和未获取到的数量notGot
Connection connection = pool.fetchConnection(1000);
if (connection != null) {
// try {
// connection.createStatement();
PreparedStatement preparedStatement
= connection.prepareStatement("");
preparedStatement.execute();
// connection.commit();
// } finally {
// pool.releaseConnection(connection);
// got.incrementAndGet();
// }
SleepTools.ms(2);
pool.releaseConnection(connection);
got.incrementAndGet();
} else {
notGot.incrementAndGet();
System.out.println(Thread.currentThread().getName()
+"等待超时!");
}
} catch (Exception ex) {
} finally {
count--;
}
}
end.countDown();
}
}
}
客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。
它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用fetchConnection(long)方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用releaseConnection(Connection)方法将连接放回线程池。
提问:
调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?