可以设想这样一种情况:取款时,账务余额不够了;顺理成章的解决方案是:等待存入足够的钱以后再进行取款操作。(这种业务场景很多,不要仅仅局限于银行存取款喽)
这儿涉及线程间通信。
如下示例:
业务情况:
Queue类:该类相当于一个容器,其有一个变量n;
Producer类:充当生产者线程,该类负责对变量n的值进行加1运算;
Consumer类:充当消费者线程,该类负责对变量n的值进行减1运算;
或者认为Producer类负责数据的生产,Consumer类负责数据的取用;
业务要求:生产一个消费一个。
……………………………………………………
1.首先,是没有添加线程间通信的情况:
Queue类:
public class Queue {
private int n;
public synchronized int getN() {
System.out.println("消费:"+n);
return n;
}
public synchronized void setN(int n) {
System.out.println("生产:"+n);
this.n = n;
}
}
Producer类,生产类:
// Queue类中包含 创造数据和获取数据两个方法(或称两个业务),若想让两个业务各自相对独立,需要涉及两个独立的线程类,分别对应两个业务的逻辑方法
// 这儿,演示意味浓重,在实际开发中,具体哪些逻辑代码需要设置成一个线程,需要慢慢累积经验
public class Producer implements Runnable{
Queue queue; // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
public Producer(){}
public Producer(Queue queue){
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while(true){
queue.setN(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Consumer类,消费类:
public class Consumer implements Runnable{
Queue queue; // 定义一个Queue类型的变量,再实例化本线程类的时候,该变量会指向线程类实际指向的Queue对象
public Consumer(){}
public Consumer(Queue queue){
this.queue = queue;
}
@Override
public void run() {
while(true){
queue.getN();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Test类,测试类:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Queue queue = new Queue();
// (1)这个例子,主要说明,在实际业务中,两个进程可以交叉运行,交叉运行的控制机制不能单靠sleep()等控制执行时间的方式;
// 如果两个线程是互相配合的情况,还需要两个线程互相交流一些数据,只有如此,才能在符合业务逻辑需求的情况下,正确地执行程序
// (2)下面两个线程,因为线程方法处添加了synchronized同步关键字,这可以保证,在一个方法执行完毕前该线程不会被中途打断;
// 但,如果当方法(本次)执行完毕,调用的sleep()时间过去后,下一次到底是执行生产方法还是消费方法,是随机的,并没有参考n的值到底是多少;(这个sleep()仅仅是
// 为了演示方便,拖长方法的执行时间,否则输出结果滑太快,看不清楚输出)
// (3)也可以发现,n的值是生产类初始化和控制的,消费类只是去获取n的值而已;按照逻辑是,生产类每生产一个,消费类紧接着就消费一个;但因为,生产类和消费
// 类之间到底该执行谁,没有一个可以完成上述业务需求的逻辑控制,所以会出现逻辑问题。
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
执行结果(只截取了前面几行):通过结果可以发现,第三行结果应该是生产1,即生产0,消费0之后,按照业务逻辑来说应该执行生产1,消费1…;但显然,第二行执行完后,生产和消费两个线程(在没有逻辑控制的)下一步该执行哪个是完全随机的,这种执行流程是不符合业务要求的。因此,有必要使得两个线程间能够彼此通信,从而使的线程间的执行顺序不再充满随机性,而是按照一定的逻辑执行;由此也是,线程间有点相互参照和依赖的味道。
生产:0
消费:0
消费:0
生产:1
消费:1
生产:2
生产:3
消费:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
生产:7
消费:7
消费:7
生产:8
2.其次,线程间通信:
●wait()方法:中断方法的执行,使线程等待;如果只有wait()方法,可能会出现:生产线程等待消费线程执行,消费线程等待生产线程执行,这样互相等待,谁都不会执行,就会处于死锁的状态;
●notify()方法:唤醒处于等待的某一个线程,使其结束等待;
●notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待;这两个方法和wait()方法对应使用;
(1)只使用wait()方法,而没有使用notify()和notifyAll()的情况:
Queue类:该类添加了flag变量,并在方法中添加了wait()方法和控制逻辑,但没有使用notify()和notifyAll();
public class Queue {
private int n;
/**
* (不提倡再变量上用这种注释方式)
* flag:
* (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
* (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
*/
boolean flag = false;
public synchronized int getN() {
// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
if(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("消费:"+n);
// 消费完毕后,需要把falg设置称false,表示没有数据了
flag = false;
return n;
}
public synchronized void setN(int n) {
// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
if(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("生产:"+n);
this.n = n;
// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
flag = true;
}
}
执行结果:下面是所有的输出结果,发现,其实现了生产一个消费一个的逻辑;但当生产7后,并没有继续出现消费7,这就是出现了死锁的情况。
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
解决办法:添加notifyAll();时刻不忘唤醒线程。(注:至于为什么会出现死锁,暂时还不明白)
public class Queue {
private int n;
/**
* (不提倡再变量上用这种注释方式)
* flag:
* (1)flag初始值为false,flase代表Queue里面没有数据,此时不能消费数据,只能生产数据;
* (2)flag为true代表Queue里有数据,此时不能再生产数据了,只能消费数据,等到数据被消费了,flag编程false后才能去生产数据
*/
boolean flag = false;
public synchronized int getN() {
// 当flag为false的时候,没有数据,就无法消费,该方法不能执行,需要等待生产方法的执行;
if(!flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("消费:"+n);
// 消费完毕后,需要把falg设置称false,表示没有数据了
flag = false;
// 唤醒线程
notifyAll();
return n;
}
public synchronized void setN(int n) {
// 当flag为true的时候,有数据了,不能再生产了,需要等待消费方法的执行;数据被消费后,flag编程false后生产方法才能执行
if(flag){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("生产:"+n);
this.n = n;
// 数据生产完毕后,需要把falg设置称true,表示已经有数据了
flag = true;
// 唤醒线程
notifyAll();
}
}
执行结果:只截取了部分,发现,程序的死锁情况解决了。
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
消费:7
生产:8
消费:8
生产:9
消费:9
生产:10
消费:10
生产:11
消费:11
生产:12
消费:12
生产:13
消费:13
注:(1)具体线程间通信,还需要通过实际业务来加深;这些可以应用哪些场景,应用时有哪些技巧等等都需要慢慢总结;
(2)线程间通信的场景也是千变万化,具体为了实现线程间通信,如何涉及逻辑代码,如何保证安全和效率的情况下的编写技巧和编写习惯做法,都需要慢慢积累;
本文探讨了如何通过线程间通信解决银行存款取款场景中的同步问题,从无通信到使用wait(), notify()和notifyAll()方法,逐步实现生产者和消费者按需执行。通过示例展示了死锁问题及解决策略,强调了在多线程编程中的协作与逻辑控制的重要性。
2470

被折叠的 条评论
为什么被折叠?



