JDK并发策略

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK并发策略相关的知识,希望对你有一定的参考价值。

名词解释

并发(Concurrency)与并行(Parallelism)

并发偏重于多个任务交替执行,而多个任务之间有可能存在还是串行的;
并行是真正意义上的同时执行;
如果只有一个cpu是不可能真实并行的。

临界区

共享资源

死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁 :多个线程之间相互出现等锁释放的场景,长期处于饥饿状态;
饥饿 :是指某一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。如优先级太低。饥饿还是有可能在未来的一段时间内解决的。
活锁:秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时获得所有资源而正常执行

线程状态图

技术分享图片
当new出一个线程时,其实线程并没有工作。它只是生成了一个实体,当你调用这个实例的start方法时,线程才真正地被启动。启动后到Runnable状态,Runnable表示该线程的资源等等已经被准备好,已经可以执行了,但是并不表示一定在执行状态,由于时间片轮转,该线程也可能此时并没有在执行。对于我们来说,该线程可以认为已经被执行了,但是是否真实执行,还得看物理cpu的调度。当线程任务执行结束后,线程就到了Terminated状态。

有时候在线程的执行当中,不可避免的会申请某些锁或某个对象的监视器,当无法获取时,这个线程会被阻塞住,会被挂起,到了Blocked状态。如果这个线程调用了wait方法,它就处于一个Waiting状态。进入Waiting状态的线程会等待其他线程给它notify,通知到之后由Waiting状态又切换到Runnable状态继续执行。当然等待状态有两种,一种是无限期等待,直到被notify。一直则是有限期等待,比如等待10秒还是没有被notify,则自动切换到Runnable状态。

Thread.State{
        New,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
}

技术分享图片

New -> RUNNABLE
线程实例调用 start 方法即可进入可运行状体。
如果正在执行的线程运行到 yield 方法,则让出 CPU 时间片,但状态还是 RUNNABLE。

RUNNABLE -> BLOCKED
通常调用 synchronized 方法,或者尝试进入 synchronized 程序时,又没抢占到 Object 的 Monitor 锁时,会产生这种状态迁移。

RUNNABLE -> WAITING
线程调用 Object.wait() / LockSupport.park() / Thread.join() 方法。

其中 join 方法是 synchronized 方法,内部调用当前线程对象的 wait 方法。

RUNNABLE -> TIMED_WAITING
情况和 RUNNABLE -> WAITING 类似,但是增加了 Thread.sleep 情况,调用 sleep 时 不会释放 当前线程抢占到的 Monitor 锁,所以当 sleep 结束后,依旧进入 RUNNABLE 状态。

WAITING -> RUNNABLE
其他线程调用 Object.notify() / Object.notifyAll() 时,某等待线程被唤醒,同时抢到等待对象的 Monitor锁。

WAITING -> BLOCKED
其他线程调用 Object.notify() / Object.notifyAll() 时,某等待线程被唤醒,同时没有抢到等待对象的 Monitor锁,只能被阻塞等待其他线程释放锁

Monitor 对象锁

Monitor是一个同步工具,相当于操作系统中的互斥量(mutex),即值为1的信号量。它内置与每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。

所有对象都含有单一的锁monitor。
JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数变为0。在任务(线程)第一次给对象加锁的时候,计数变为1。每当这个相同的任务(线程)在此对象上获得锁时,计数会递增。
只有首先获得锁的任务(线程)才能继续获取该对象上的多个锁。
每当任务离开一个synchronized(同步)方法,计数递减,当计数为0的时候,锁被完全释放,此时别的任务就可以使用此资源。

锁池和等待池

锁池:假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就只能进入了该对象的锁池(monitor pool)中。

_

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池!

public class WaitMonitorPooL {
    public static void main(String[] args) throws InterruptedException {
        WaitMonitorPooL waitMonitorPooL = new WaitMonitorPooL();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Long before = new Date().getTime();
                    System.out.println(Thread.currentThread().getId() +"   "+before);
                    synchronized (waitMonitorPooL){
                        waitMonitorPooL.wait();
                    }
                    Long after = new Date().getTime();
                    System.out.println(Thread.currentThread().getId()+"  " +(after -before));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Long before = new Date().getTime();
                    System.out.println(Thread.currentThread().getId() +"   "+ before);
                    synchronized (waitMonitorPooL){
                        waitMonitorPooL.wait();
                    }
                    Long after = new Date().getTime();
                    System.out.println(Thread.currentThread().getId() +"  "+(after -before));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

      new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2900L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (waitMonitorPooL) { //所有线程已经释放了对象锁
                    waitMonitorPooL.notifyAll();
                }
            }
        }) .start();
    }
}

11 1540278446740
10 1540278446740
11 2903
10 2903

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

wait() 方法

该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结构。

notify() 方法

该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。这与下面的 notifyAll()方法执行后的情况不同。

notifyAll() 方法

该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

sleep方法

sleep()方法的过程中,线程不会释放对象锁,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备;

以上是关于JDK并发策略的主要内容,如果未能解决你的问题,请参考以下文章

golang goroutine例子[golang并发代码片段]

Java并发-- ConcurrentHashMap如何实现高效地线程安全(jdk1.8)

CopyOnWriteArrayList并发容器

如何从设置中获取数据并发送到此片段

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题