乐趣区

关于java:多线程基础知识

​本节内容:

线程的状态

wait/notify/notifyAll/sleep 办法的介绍

如何正确进行线程

有哪些实现生产者消费者的办法

<span id=”jump1″> 线程的状态 /span>

线程一共有六种状态,别离是 New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(期待)、Timed WaitIng(计时期待)、Terminated(终结)

状态流转图

NEW(新建)

当咱们 new 一个新线程的时候,如果还未调用 start() 办法,则该线程的状态就是 NEW,而一旦调用了 start() 办法,它就会从 NEW 变成 Runnable

Runnable(可运行)

java 中的可运行状态分为两种,一种是可运行,一种是运行中,如果以后线程调用了 start() 办法之后,还未获取 CPU 工夫片,此时该线程处于可运行状态,期待被调配 CPU 资源,如果取得 CPU 资源后,该线程就是运行状态。

Blocked(阻塞)

java 中的阻塞也分三种状态:Blocked(被阻塞)、Waiting(期待)、Timed Waiting(计时期待), 这三种状态统称为阻塞状态。

  • Blocked 状态 (被阻塞):从联合图中能够看出从 Runnable 状态进入 Blocked 状态只有进入 synchronized 爱护的代码时,没有获取到锁 monitor 锁,就会处于 Blocked 状态
  • Time Waiting(计时期待):Time Waiting 和 Waiting 状态的区别是有没有工夫的限度,一下状况会进入 Time Waiting:
  • 设置了工夫参数的 Thread.sleep(long millis)
  • 设置了工夫参数的 Object.wait(long timeout)
  • 设置了工夫参数的 Thread.join(long millis)
  • 设置了工夫参数的 LockSupport.parkNanos(long millis) 和 LockSupport.parkUntil(long deadline)
  • Waiting 状态 (期待):线程进入 Waiting 状态有三种状况,别离是:
  • 没有设置 Timeout 的 Object.wait() 办法
  • 没有设置 Timeout 的 Thread.join() 办法
  • LockSupport.park() 办法

Blocked 状态仅仅针对 synchronized monitor 锁,如果获取的锁是 ReentrantLock 等锁时,线程没有抢到锁就会进入 Waiting 状态,因为实质上它执行的是 LockSupport.park() 办法,所以会进入 Waiting 办法,同样 Object.wait()、Thread.join() 也会让线程进入 waiting 状态。Blocked 和 Waiting 不同的是 blocked 期待其余线程开释 monitor 锁,而 Waiting 则是期待某个条件,相似 join 线程执行结束或者 notify()\notifyAll()。

上图中能够看出处于 Waiting、Time Waiting 的线程调用 notify() 或者 notifyAll() 办法后,并不会进入 Runnable 状态而是进入 Blocked 状态,因为唤醒处于 Waiting、Time Waiting 状态的线程的线程在调用 notify() 或者 notifyAll() 时候,必须持有该 monitor 锁,所以处于 Waiting、Time Waiting 状态的线程被唤醒后,就会进入 Blocked 状态,直到执行了 notify()\notifyAll() 的线程开释了锁,被唤醒的线程才能够去争夺这把锁,如果抢到了就从 Blocked 状态转换到 Runnable 状态

Terminated(终结)

进入这个状态的线程分两种状况:

  1. run() 办法执行结束,失常退出
  2. 产生异样,终止了 run() 办法。

<span id=”jump2″>wait/notify/notifyAll 办法的应用 </span>

首先 wait 办法必须在 sychronized 爱护的同步代码中应用, 在 wait 办法的源码正文中就有说:

在应用 wait 办法是必须把 wait 办法写在 synchronized 爱护的 while 代码中, 并且始终判断执行条件是否满足, 如果满足就持续往下执行, 不满足就执行 wait 办法, 而且执行 wait 办法前, 必须先持有对象的 synchronized 锁.

下面次要是两点:

  1. wait 办法要在 synchronized 同步代码中调用.
  2. wait 办法应该总是被调用在一个循环中

咱们先剖析第一点, 联合以下场景剖析为什么要这么设计

public class TestDemo {private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){storage.add(data);
        notify();}

public String remove() throws InterruptedException {
//wait 不必 synchronized 关键字爱护,间接调用,while (storage.isEmpty()){wait();
        }
return storage.remove();}
}

上述代码是一个简略的基于 ArrayBlockingQueue 实现的生产者、消费者模式, 生产者调用 add(String data) 办法向 storage 中增加数据, 消费者调用 remove() 办法从 storage 中生产数据.

代码中咱们能够看到如果 wait 办法的调用没有用 synchronized 爱护起来, 那么就可能产生一下场景状况:

  1. 消费者线程调用 remove() 办法判断 storage 是否为空, 如果是就调用 wait 办法, 消费者线程进入期待, 然而这就可能产生消费者线程调用完 storage.isEmpty() 办法后就被调度器暂停了, 而后还没来得及执行 wait 办法.
  2. 此时生产者线程开始运行, 开始执行了 add(data) 办法, 胜利的增加了 data 数据并且执行了 notify() 办法, 然而因为之前的消费者还没有执行 wait 办法, 所以此时没有线程被唤醒.
  3. 生产者执行结束后, 方才被调度器暂停的消费者再回来执行 wait 办法, 并且进入了期待, 此时 storage 中曾经有数据了.

以上的状况就是线程不平安的, 因为 wait 办法的调用错过了 notify 办法的唤醒, 导致应该被唤醒的线程无奈收到 notify 办法的唤醒.

正是因为 wait 办法的调用没有被 synchronized 关键字爱护, 所以他和 while 判断不是原子操作, 所以就会呈现线程平安问题.

咱们把以上代码改成如下, 就实现了线程平安

public class TestDemo {private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){synchronized (this){storage.add(data);
            notify();}
    }

public String remove() throws InterruptedException {synchronized (this){while (storage.isEmpty()){wait();
            }
return storage.remove();}
    }
}

咱们再来剖析第二点 wait 办法应该总是被调用在一个循环中?

之所以将 wait 办法放到循环中是为了避免线程“虚伪唤醒“(spurious wakeup), 线程可能在没有被 notify/notyfiAll, 也没有被中断或者超时的状况下被唤醒, 尽管这种概率产生十分小, 然而为了保障产生虚伪唤醒的正确性, 所以须要采纳循环构造, 这样即使线程被虚伪唤醒了, 也会再次查看 while 的条件是否满足, 不满足就调用 wait 办法期待.

为什么 wait/notify/notifyAll 被定义在 Object 类中

java 中每个对象都是一个内置锁, 都持有一把称为 monitor 监视器的锁, 这就要求在对象头中有一个用来保留锁信息的地位. 这个锁是对象级别的而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作, 它们的锁属于对象, 所以把它们定义在 Object 中最合适.

wait/notify 和 sleep 办法的异同

相同点:

  1. 它们都能够让线程阻塞
  2. 它们都能够响应 interrupt 中断: 在期待过程中如果收到中断信号, 都能够进行响应并抛出 InterruptedException 异样

不同点:

  1. wait 办法必须在 synchronized 同步代码中调用,sleep 办法没有这个要求
  2. 调用 sleep 不会开释 monitor 锁, 调用 wait 办法就开释 monitor 锁
  3. sleep 要求期待一段时间后会主动复原, 然而 wait 办法没有设置超时工夫的话会始终期待, 直到被中断或者被唤醒, 否则不能被动复原
  4. wait/notify 是 Object 办法,sleep 是 Thread 的办法

<span id=”jump3″> 如何正确进行线程 </span>

正确的进行线程形式是通过应用 interrupt 办法,interrupt 办法仅仅起到了告诉须要被中断的线程的作用, 被中断的线程有齐全的自主权, 它能够立即进行, 也能够执行一段时间再进行, 或者压根不进行. 这是因为 java 心愿程序之间能相互告诉、合作的实现工作.

interrupt() 办法的应用

public class InterruptDemo implements Runnable{public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new InterruptDemo());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();}

    @Override
    public void run() {
        int i =0;
        while (!Thread.currentThread().isInterrupted() && i<1000){System.out.println(i++);
        }
    }
}

上图中通过循环打印 0~999, 然而理论运行并不会打印到 999, 因为在线程打印到 999 之前, 咱们对线程调用了 interrupt 办法使其中断了, 而后依据 while 中的判断条件, 办法提前终止, 运行后果如下:

其中如果是通过 sleep、wait 办法使线程陷入休眠, 处于休眠期间的线程如果被中断是能够感触到中断信号的, 并且会抛出一个 InterruptException 异样, 同时革除中断信号, 将中断标记位设置为 false.

<span id=”jump3″> 有哪些实现生产者消费者的办法 </span>

生产者消费者模式是程序设计中常见的一种设计模式, 咱们通过下图来了解生产者消费者模式:

应用 BolckingQueue 实现生产者消费者模式

通过利用阻塞队列 ArrayBlockingQueue 实现一个简略的生产者消费者模式, 创立两个线程用来生产对象, 两个线程用来生产对象, 如果 ArrayBlockingQueue 满了, 那么生产者就会阻塞, 如果 ArrayBlockingQueue 为空, 那么消费者线程就会阻塞. 线程的阻塞和唤醒都是通过 ArrayBlockingQueue 来实现的.

public void MyBlockingQueue1(){BlockingQueue<Object> queue=new ArrayBlockingQueue<>(10);
        Runnable producer = () ->{while (true){
                try {queue.put(new Object());
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        };
        new Thread(producer).start();
        new Thread(producer).start();

        Runnable consumer = () ->{while (true){
                try {queue.take();
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        };
        new Thread(consumer).start();
        new Thread(consumer).start();}

应用 Condition 实现生产者消费者模式

如下代码其实也是相似 ArrayBlockingQueue 外部的实现原理.

如下代码所示, 定义了一个队列容量是 16 的的 queue, 用来存放数据, 定义一个 ReentrantLock 类型的锁, 并在 Lock 锁的根底上创立了两个 Condition, 一个是 notEmpty 一个是 notFull, 别离代表队列没有空和没有满的条件, 而后就是 put 和 take 办法.

put 办法中, 因为是多线程拜访环境, 所以先上锁, 而后在 while 条件中判断 queue 中是否曾经满了, 如果满了, 则调用 notFull 的 await() 办法阻塞生产者并开释 Lock 锁, 如果没有满则往队列中放入数据, 并且调用 notEmpty.singleAll() 办法唤醒所有的消费者线程, 最初在 finally 中开释锁.

同理 take 办法和 put 办法相似, 同样是先上锁, 在判断 while 条件是否满足, 而后执行对应的操作, 最初在 finally 中开释锁.

public class MyBlockingQueue2 {
    private Queue queue;
    private int max;
    private ReentrantLock lock=new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull =lock.newCondition();

    public MyBlockingQueue2(int size){
        this.max =size;
        queue = new LinkedList();}

    public void put(Object o) throws InterruptedException {lock.lock();
        try {while (queue.size() == max) {notFull.await();
            }
            queue.add(o);
            // 唤醒所有的消费者
            notEmpty.signalAll();} finally {lock.unlock();
        }
    }

    public Object take() throws InterruptedException{lock.lock();
        try {
        // 这里不能改用 if 判断, 因为生产者唤醒了所有的消费者,
        // 消费者唤醒后, 必须在进行一次条件判断
            while (queue.size() == 0) {notEmpty.await();
            }
            Object remove = queue.remove();
            // 唤醒所有的生产者
            notFull.signalAll();
            return remove;
        }finally {lock.unlock();
        }
    }
}

应用 wait/notify 实现生产者消费者模式

如下代码所示, 利用 wait/notify 实现生产者消费者模式次要是在 put 和 take 办法上加了 synchronized 锁, 并且在各自的 while 办法中进行条件判断


public class MyBlockingQueue3 {
    private int max;
    private Queue<Object> queue;

    public MyBlockingQueue3(int size){
        this.max =size;
        this.queue=new LinkedList<>();}

    public synchronized void put(Object o) throws InterruptedException {while(queue.size() == max){wait();
        }
        queue.add(o);
        notifyAll();}

    public synchronized Object take() throws InterruptedException {while (queue.size() == 0){wait();
        }
        Object remove = queue.remove();
        notifyAll();
        return remove;
    }
}

以上就是三种实现生产者消费者模式的形式, 第一种比较简单间接利用 ArrayBlockingQueue 外部的特色实现生产者消费者模式的实现场景, 第二种是第一种背地的实现原理, 第三种利用 synchronzied 实现.

退出移动版