⭐写在前面⭐
内容回顾
Java 多线程介绍及线程创建
Java 多线程七大状态
Java 多线程方法详解
Java synchronized关键字实现线程同步
今天我们进行 JDBC 获取数据库连接的5种方式 的学习,感谢你的阅读,内容若有不当之处,希望大家多多指正,一起进步!!!
♨️如果觉得博主文章还不错,可以三连支持⭐一下哦
在Java的Object类中提供了
wait
、notify
、notifyAll
等方法,这些方法可以实现线程间的通信,因为Object类是所有类的基类,因此所有的对象都具有线程间通信的方法。
void wait()
:调用一个对象的wait方法,会导致当前持有该对象的锁的线程等待,直到该对象的另一个持有锁的线程调用notify
或者notifyAll
唤醒。
void wait(long timeout)
:除了和wait相似,还具有超过定时的超时时间,时间到后或自动唤醒。
void wait(long timeout,int nanou)
:与 void wait(long timeout) 相同,不过提供了纳秒级别的更精确的超时控制。
void notify()
:调用一个对象的notify方法,会导致当前持有该锁的所有线程中的随机某一个线程被唤醒。
void notifyAll()
:调用一个对象的notifyAll方法,会导致当前持有该锁的所有线程被唤醒。
通信是在不同线程间的通信,一个线程处于
wait
状态阻塞等待被唤醒,另一个线程通过notify
或者notifyAll
唤醒,当前的唤醒操作必须是作用与同一个对象,注意在进行唤醒和阻塞时必须要加锁的,加锁需要使用synchronized
关键字。
WaitDemo类
public class WaitDemo extends Thread{
private Object obj;
public WaitDemo(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "WaitDemo执行开始~~~");
try {
obj.wait(); //调用wait方法阻塞线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "WaitDemo执行结束~~~");
}
}
}
NotifyDemo类
public class NotifyDemo extends Thread{
private Object obj;
public NotifyDemo(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "NotifyDemo执行开始~~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.notify(); //调用notify方法唤醒阻塞线程
System.out.println(Thread.currentThread().getName() + "NotifyDemo执行结束~~~");
}
}
}
TestWaitAndNotify测试类
public class TestWaitAndNotify {
public static void main(String[] args) {
Object object = new Object();
WaitDemo waitDemo = new WaitDemo(object);
NotifyDemo notifyDemo = new NotifyDemo(object);
waitDemo.setName("WaitDemo线程");
notifyDemo.setName("NotifyDemo线程");
waitDemo.start();
notifyDemo.start();
}
}
调用
notify
和wait
方法必须是作用于同一个对象,如果不是通一个对象则无法完成通信。
对于wait
、nitify
、notifyAll
的调用,必须在该对象的同步方法或者代码块中,锁作用的对象和wait
等方法必须是作用于同一个对象。
wait
方法在调用后进入阻塞之前会释放锁,而sleep和join是不会释放锁的。
线程状态转换时,当wait
被唤醒或者超时时,线程并不是直接进入就绪状态,而是先进入阻塞状态,抢锁成功后才能进入到可运行状态。
注意点1 当锁的对象和调用
wait
、notify
的对象不是同一个对象时,会抛出IllegalMonitorStateException
异常。
注意点2
wait
方法在调用进入阻塞之前会释放锁
基于以上分析,一旦wait线程先调用则线程因为锁无法继续执行而阻塞下来,实际上notify依然可以获取锁进行执行,这是因为wait方法在调用进入阻塞之前释放锁,则调用notify操作的线程就可以抢到Object对象的锁,进而调用notify。
注意点3 锁池和等待池
锁池:假设线程A已经拥有了某个对象的锁,而其他的线程想要调用这个对象的某个synchronized方法,由于这些线程在进入对象的synchronized方法之前必须先获取该对象的锁的拥有权,但是该对象的锁目前被线程A拥有,所以这些线程就回去进入到该对象的锁池
等待池:假设一个线程A调用某个对象的wait方法,线程A就会释放该对象的锁后,进入到该对象的等待池
有三个线程,分别为ABC线程,需要线程交替打印:ABCABC…打印10遍
分析:需要使用线程间的通信,A给B通信,A进行notifyB进行wait;B给C通信,B进行notifyC进行Wait;同理C给A通信,C进行notifyA进行wait。
思路分析:给每个线程给定编号,表明是第几个进程,再给定一个共享对象,共享对象进行notify、wait等操作,共享对象本身需要携带信息表明下一个执行的线程编号,如果当前线程的编号与共享对象中的信息比较,如果相等就执行,否则就阻塞。
代码示例
NextOpt类:共享对象,携带下一个要执行的线程编号信息
//共享对象,携带下一个要执行的线程编号信息
public class NextOpt {
//下一个执行线程编号
private Integer nextValue;
public Integer getNextValue() {
return nextValue;
}
public void setNextValue(Integer nextValue) {
this.nextValue = nextValue;
}
}
ThreadABC类
public class ThreadABC extends Thread {
//线程间通信对象
private NextOpt opt;
//打印名称通过数组获取
private String[] abc = {"A", "B", "C"};
//线程编号
private int index;
//执行次数
int count = 0;
public ThreadABC(NextOpt opt, int index) {
this.opt = opt;
this.index = index;
}
@Override
public void run() {
while (true) { //如果当前线程编号与下一个要执行的序号不一致就阻塞
synchronized (opt) {
while (opt.getNextValue() != index) {
try {
opt.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印
System.out.print(abc[index]);
//设置下一个要执行的线程的编号
opt.setNextValue((index + 1) % 3);
//唤醒所有的阻塞线程
opt.notifyAll();
if (count++ > 9) {
break;
}
}
}
}
}
TestThreadABC测试类
public class TestThreadABC {
public static void main(String[] args) {
NextOpt opt = new NextOpt();
opt.setNextValue(0); //设置第一个要执行的线程编号
ThreadABC thread1 = new ThreadABC(opt,0);
ThreadABC thread2 = new ThreadABC(opt,1);
ThreadABC thread3 = new ThreadABC(opt,2);
thread1.start();
thread2.start();
thread3.start();
}
}
线程间通信典型案例:生产者消费者模型
生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。
其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。
问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。
同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
再具体一点:
a.生产者生产数据到缓冲区中,消费者从缓冲区中取数据。
b. 如果缓冲区已经满了,则生产者线程阻塞。
c. 如果缓冲区为空,那么消费者线程阻塞。
上述过程的描述应该已经体现出生产者和消费者之间的线程通信的流程,生产者一旦将队列生成满了之后就要控制线程停止生产,直到消费者将队列中消费一个之后就可以通知生产者继续生产新的元素,当消费者线程将队列中的元素全部取出之后消费者线程就需要停止消费元素,直到生产者线程向队列中添加一个元素之后可以通知消费者线程继续消费元素。
编写一个生产者、消费者模型,给定要求:一个生产者、一个消费者、仓库是三个
生产者
public class Producer extends Thread{
private LinkedList<Integer> cap;//共享仓库
private Random random = new Random();
public Producer(LinkedList<Integer> cap) {
this.cap = cap;
}
@Override
public void run() {
while (true) {
synchronized (cap){
if (cap.size() == 3) {//缓冲区满 生产者进行阻塞
try {
cap.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产产品
int i = random.nextInt(1000);
System.out.println("生产者生产了" + i);
cap.add(i);
//通知消费者消费产品
cap.notify();
}
}
}
}
消费者
public class Consumer extends Thread{
private LinkedList<Integer> cap;
public Consumer(LinkedList<Integer> cap) {
this.cap = cap;
}
@Override
public void run() {
while (true) {
synchronized (cap) {
if (cap.size() == 0) { //如果缓冲区为0,消费者阻塞
try {
cap.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费者消费产品
Integer i = cap.remove();
System.out.println("消费者消费了" + i);
//通知生产者生产
cap.notify();
}
}
}
}
测试类
public class Test {
public static void main(String[] args) {
LinkedList<Integer> cap = new LinkedList<>();
Producer producer = new Producer(cap);
Consumer consumer = new Consumer(cap);
producer.start();
consumer.start();
}
}