JAVA并发四(并发的部分应用)

1.同步模式之保护性暂停 

1.1.定义

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,要点:

  1. 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject

  2. 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)

  3. JDK 中,join 的实现、Future 的实现,采用的就是此模式

  4. 因为要等待另一方的结果,因此归类到同步模式

 1.2.举例

JAVA并发四(并发的部分应用)

 

public class ProtectSyn {
    public static void main(String[] args) {
        String hello = "hello thread!";
        Guarded guarded = new Guarded();
        new Thread(()->{
            System.out.println("想要得到结果");
            synchronized (guarded) {
                System.out.println("结果是:"+guarded.getResponse());
            }
            System.out.println("得到结果");
        }).start();
        new Thread(()->{
            System.out.println("设置结果");
            synchronized (guarded) {
                guarded.setResponse(hello);
            }
        }).start();
    }
}
class Guarded {
    /**
     * 要返回的结果
     */
    private Object response;
    //优雅地使用wait/notify
    public Object getResponse() {
//如果返回结果为空就一直等待,避免虚假唤醒
        while(response == null) {
            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return response;
    }
    public void setResponse(Object response) {
        this.response = response;
        synchronized (this) {
//唤醒休眠的线程
            this.notifyAll();
        }
    }
    @Override
    public String toString() {
        return "Guarded{" +
                "response=" + response +
                '}';
    }
}

带超时判断的暂停:

    public Object getResponse(long time) {
        synchronized (this) {
            //获取开始时间
            long currentTime = System.currentTimeMillis();
            //用于保存已经等待了的时间
            long passedTime = 0;
            while(response == null) {
                //看经过的时间-开始时间是否超过了指定时间
                long waitTime = time - passedTime;
                if(waitTime <= 0) {
                    break;
                }
                try {
                    //等待剩余时间
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取当前时间
                passedTime = System.currentTimeMillis()-currentTime
            }
        }
        return response;
    }

thread.join()方法中就利用了带超时判断的保护性暂停

    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0L;
        if (millis < 0L) {
            throw new IllegalArgumentException("timeout value is negative");
        } else {
            if (millis == 0L) {
                while(this.isAlive()) {
                    this.wait(0L);
                }
            } else {
                while(this.isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0L) {
                        break;
                    }

                    this.wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }

        }
    }

 1.3 异步模式之生产者消费者模式:

要点

  1. 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应

  2. 消费队列可以用来平衡生产和消费的线程资源

  3. 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据

  4. 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

  5. JDK 中各种阻塞队列,采用的就是这种模式

        “异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。

        简而言之,异步模式的生产者的消息被消息队列给消费了,消费队列消费了消息之后,又把消息转手给了消费者,消息队列充当了中间商的角色。

        同步模式就是没有中间商赚差价,一对一直销。

JAVA并发四(并发的部分应用)

 

2.park() 和 unpark()方法:

2.1 基本使用:

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark;

2.2 原理

每个线程都有一个自己的Park对象,并且该对象_counter, _cond,__mutex组成
        先调用park再调用unpark时
                先调用park
                        线程运行时,会将Park对象中的_counter的值设为0;
                        调用park时,会先查看counter的值是否为0,如果为0,则将线程放入阻塞队列cond中
                        放入阻塞队列中后,会再次将counter设置为0
                然后调用unpark
                        调用unpark方法后,会将counter的值设置为1
                        去唤醒阻塞队列cond中的线程
                        线程继续运行并将counter的值设为0 

  1. 打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量 _ cond就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)

  2. 调用 park 就是要看需不需要停下来歇息

    1. 如果备用干粮耗尽,那么钻进帐篷歇息

    2. 如果备用干粮充足,那么不需停留,继续前进

  3. 调用 unpark,就好比令干粮充足

    1. 如果这时线程还在帐篷,就唤醒让他继续前进

    2. 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进

      1. 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

 3.线程中的状态转换

JAVA并发四(并发的部分应用)

情况一:NEW –> RUNNABLE
        当调用了t.start()方法时,由 NEW –> RUNNABLE
情况二: RUNNABLE <–> WAITING
        当调用了t 线程用 synchronized(obj) 获取了对象锁后
                调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
                调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
                        竞争锁成功,t 线程从 WAITING –> RUNNABLE
                        竞争锁失败,t 线程从 WAITING –> BLOCKED
情况三:RUNNABLE <–> WAITING
        当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
                注意是当前线程在t 线程对象的监视器上等待
        t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
情况四: RUNNABLE <–> WAITING
        当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
        调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –>RUNNABLE
情况五: RUNNABLE <–> TIMED_WAITING
        t 线程用 synchronized(obj) 获取了对象锁后
                调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
        t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
                竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
                竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
情况六:RUNNABLE <–> TIMED_WAITING
        当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
                注意是当前线程在t 线程对象的监视器上等待
        当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING –> RUNNABLE
情况七:RUNNABLE <–> TIMED_WAITING
        当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
        当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
情况八:RUNNABLE <–> TIMED_WAITING
        当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
        调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
情况九:RUNNABLE <–> BLOCKED
        t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
        持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况十: RUNNABLE <–> TERMINATED
        当前线程所有代码运行完毕,进入 TERMINATED

4.活跃性

        因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性,活跃性相关的一系列问题都可以用ReentrantLock进行解决。

4.1死锁

        一个线程需要同时获取多把锁,这时就容易发生死锁t1 线程获得A对象锁,接下来想获取B对象的锁;t2 线程获得B对象锁,接下来想获取A对象的锁例。

public class DeadLock {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        new Thread(){
            @Override
            public void run(){
                //拿o1的锁
                synchronized (o1){
                    try {
                        //等待一段时间保证o1拿到锁不会立马释放。
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //再去拿o2的锁 而此时o2的锁已经被另外一个线程持有。
                    synchronized (o2){

                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run(){
                //拿o2的锁
                synchronized (o2){
                    try {
                        //等待一段时间保证o2拿到锁不会立马释放。
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //再去拿o1的锁 而此时o1的锁已经被另外一个线程持有。
                    synchronized (o1){

                    }
                }
            }
        }.start();
    }
}

发生死锁的必要条件
        互斥条件
                在一段时间内,一种资源只能被一个进程所使用
        请求和保持条件
                进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
        不可抢占条件
                进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
        循环等待条件
                发生死锁时,必然存在一个进程——资源的循环链。

 4.2哲学家就餐问题(锁经典问题)

 JAVA并发四(并发的部分应用)

 4.3活锁

活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束。
        避免活锁的方法: 在线程执行时,中途给予不同的间隔时间即可。
死锁与活锁的区别
        死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
        活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

4.4饥饿

        某些线程因为优先级太低,导致一直无法获得资源的现象。
        在使用顺序加锁时,可能会出现饥饿现象

5.ReentrantLock 

java.util.concurrent.locks.ReentrantLock;

和synchronized相比具有的的特点
        可中断
        可以设置超时时间
        可以设置为公平锁 (先到先得)
        支持多个条件变量( 具有多个waitset)

与 synchronized 一样,都支持可重入。

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

可重入

        可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

可打断

        如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败
简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行。

锁超时
        使用lock.tryLock方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。
        并且tryLock方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中timeout为最长等待时间,TimeUnit为时间单位简而言之就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行。

        

附:集合并发可能遇到的错误:

上一篇:Waiting for cache lock


下一篇:jca457分析线程堆栈