Java养成计划(打卡第27天)
分享之前的思考:
最近不断学习Java基础,最开始是看视频,mooc,B站各类视频都看了,将Java se给过了一遍,确实广度上去了,并且有了一定的深度,也开拓了一些思路,比如UML,设计模式,之后又借了很多书看,发现确实 书本的内容更加翔实,让我自觉还有很多没有掌握,但是各种不同的书籍给人的感觉真的不同,并且对于某个细节具有不同的表述,让人眼前一亮。每个对象有且仅有一把🔒。
比如在例外处理中的try catch,打印例外信息,就很多中,比如直接使用例外类的实例方法,或者使用toString,,,总之解决同一个问题有很多种思路,还有比如倒计时,除了使用System的方法之外,也可以简单一点,直接累计时间就好了,但是system的方法更精确吧,但是由于CPU的随机性,还是不那么精确。
进程是一个程序的一次动态执行,对应程序从代码加载,执行,完毕整个过程
线程是进程执行过程种产生的多条执行线索。 一个线程局势一个程序内部的顺序控制流
线程同步
之前已经分析过synchronized互斥锁产生临界区,保证线程之间不会同时抢夺,混乱。那什么是线程同步呢?
所谓的线程同步是指,相互合作的两个线程需要交换一定的信息,当线程没有获得合作线程发来的信息,线程就等待,一直到有消息才唤醒执行,也就是处于waiting状态,(这里涉及到通信)
这里就用生产者-消费者问题来说明线程同步
Producer—Consumer
假设有两个线程,一个消费者线程,一个生产者线程,它们共用一个有界队列
生产者生产数据放入该队列,消费者线程从队列中取出元素加以利用,该队列是有界的,所以当队列满时,生产者就要处于等待状态,直到消费者取出数据;同样,当队列为空时,消费者线程应该处于等待状态,直到生产者放入数据。
我们接下来就来模拟这个过程,涉及到的类有Producer,Consumer,队列结点类QueueNode,队列Queue
Producer类,主要定义生产者线程的run方法
Producer类中与Queue的关联关系为has-a
package ThreadDemo;
public class Producer implements Runnable{
Queue q;
public Producer(Queue q) {
this.q = q;
new Thread(this).start();
}
@Override
public void run() {
for(int i = 1;i <= 10;i++)
{
q.enqueue(Integer.valueOf(i));
System.out.println("produced :" + i);
try
{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
Consumer类,主要是定义run方法
只要调用构造方法就可以启动线程
package ThreadDemo;
final class Consumer implements Runnable{
Queue q;
public Consumer(Queue q) {
this.q = q;
new Thread(this).start();
}
@Override
public void run() {
for(int n = 1;n <= 10;n++)
{
int i = (Integer)(q.dequeue());
System.out.println("Consumed:" + i);
try
{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
起始类,主要是创建一个队列,并用队列启动线程
package ThreadDemo;
public class ConsumerTest {
public static void main(String[] args) {
Queue q = new Queue(6);
new Producer(q);
new Consumer(q);
}
}
队列结点类
其实就是C语言里的一个链表的结点,引用就是指针,所以操作和之前一样,一个存储数据的data,一个指向下一个结点的指针next
package ThreadDemo;
public class QueueNode {
Object data;
QueueNode next;
public QueueNode(Object o) {
this.data = o;
this.next = null;
}
public Object getData() {
return data;
}
}
队列类Queue
创建一个队列,有队首指针和队尾指针,队列先进先出,队首删除,队尾插入,所以插入直接使用尾插法
package ThreadDemo;
public class Queue {
private QueueNode firstNode;
private QueueNode lastNode;
private int size;
private int lenth;
public Queue(int n) {
this.size = n;
this.lenth = 0;
this.firstNode = this.lastNode = null;
}
public void enqueue(Object item)
{
while(lenth == size)
{
try
{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
if(lenth == 0)
{
firstNode = lastNode = new QueueNode(item);
}
else {
lastNode = lastNode.next = new QueueNode(item);
}
lenth++;
}
public Object dequeue()
{
while(lenth == 0)
{
try
{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
Object o = firstNode.data;
if(firstNode.equals(lastNode)) {
firstNode = lastNode = null;
}
else {
firstNode = firstNode.next;
}
lenth--;
return o;
}
}
这样之后就可以运行起始类,这里为多线程,与CPU有关
可能会抛出异常
produced :1 produced :2 Consumed:1 Exception in thread “Thread-0” Exception in thread “Thread-1”
Cannot assign field “next” because “this.lastNode” is null
NullPointerException: Cannot read field “data” because “this.firstNode” is null
这是什么原因,来模拟一下
生产者线程将数据入队,判断队列为空,所以就不沉睡,produced 1;
生产者沉睡,消费者线程和生产者之后同时进入了某段代码执行,在取出了1之后,生产者还是按照有结点的方式来创建,但是此时lastNode已经是空了,报错,不能生产,消费者也就按照有节点执行,但firtNode为空,报错
使用synchronized上锁
所以这里就必须加锁了,这里我们就给队列Queue的两个方法上锁,synchronized修饰成为临界区,只有一个线程能执行
public synchronized Object dequeue()
public synchronized void enqueue(Object item)
这样按照昨天的锁的解释,当生产者线程进入队列时,队列对象就上锁,关闭,当生产者线程出来之后,锁打开,消费者线程才能进入
这样如果情况恰当,就不会混乱
produced :1 Consumed:1 produced :2 Consumed:2 produced :3 Consumed:3 produced :4 Consumed:4 produced :5 Consumed:5 produced :6 Consumed:6 produced :7 Consumed:7 produced :8 Consumed:8 produced :9 Consumed:9 produced :10 Consumed:10
但多执行几次,发现产生新的问题
produced :1 Consumed:1 produced :2 Consumed:2
本来需要生产10个数,但这里生产两个数之后就卡住了,成为了死锁状态
这是怎么回事,这里就和对象🔒有关
比如这里Consumer线程执行拿出数据2,接下来生产者和消费者线程都可能执行,CPU给了消费者线程会怎么样?
消费者线程发现队空,进入睡眠状态,但就算sleep放弃了CPU,但是对象锁依然事关闭的,所以生产者线程不能进入 ,沉睡1s之后,因为生产者线程还在锁等待池等待,不能进入enqueue,所以队空,消费者就继续沉睡,形成了死锁
怎么解决死锁问题?
说白了,问题的关键就是sleep方法不会开锁,也即是关锁沉睡
那就应该有一个替代sleep的方法,对象状态不适合继续处理时,线程应该放弃CPU并同时开锁,进入等待状态,继续执行
- public final void wait() throws InterruptedException 使执行线程放弃CPU并释放对象🔒,进入该对象的wait等待池
- public final void notify() 从wait等待池中唤醒一个线程,并将其放入🔒等待池
- public final void notifyAll() 将wait等待池中的所有线程唤醒,并移入🔒等待池
这三个方法可以作为sleep的替代方法,并且这三个方法只能在线程持有对象锁时才能运行
锁等待池,和wait等待池的级别都不一样,wait是线程三种状态之一,而锁等待只是执行时保证互斥的手段
这里就演示了线程的状态,当t1时刻进入临界区执行同步代码并关闭对象锁,之后进入wait等待并开锁,线程2就进入临界区执行代码,关锁,给一个notify信号让线程1进入锁等待池,线程2开锁之后,线程1执行
所以这里我们就重新定义下Queue类,使用wait方法和notify方法
public synchronized void enqueue(Object item)
{
while(lenth == size)
{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(lenth == 0)
{
firstNode = lastNode = new QueueNode(item);
}
else {
lastNode = lastNode.next = new QueueNode(item);
}
lenth++;
this.notify();
}
public synchronized Object dequeue()
{
while(lenth == 0)
{
try
{
this.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
Object o = firstNode.data;
if(firstNode.equals(lastNode)) {
firstNode = lastNode = null;
}
else {
firstNode = firstNode.next;
}
lenth--;
this.notify();
return o;
}
主要变化就是将sleep换成了wait,并且在方法末尾都加上了notify唤醒
这样之后不管怎么执行都是正确的结果了,当然,这里可以改进,就是,也可以使用另外一个wait方法
- public void wait(long timeout) throws InterrptedException
规定滞留时间,如果没有notify唤醒,到时间之后就自动进入🔒等待池
sleep和wait的比较
- sleep是Thread类的静态方法,而wait是实例方法(很容易思考)
- sleep让线程放弃CPU进入睡眠状态,到时间后进入就绪状态; wait方法使线程放弃CPU进入wait等待池并释放持有的对象锁,需要用notify方法唤醒进入锁等待池,再次获得对象锁时进入就绪状态
今天的分享就到此结束了,之后会给上几个题目,然后分析解答~
|