Java并发编程学习(三)

wait与notify

注意是锁对象调用这些方法,锁对象调用wait,他们都属于同一个对象的waitset

  • Ower线程发现条件不满足,调用wait方法,即可进入WaitSet,变为Waiting状态(Ower线程就是拿到锁的线程,进入之后可以调用wait方法来让自己进入waiting状态。WaitSet是一个集合,里面专门存放waiting中的线程)
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU
  • BLOCKED线程会在Ower线程释放锁时唤醒(也就是拿到锁的线程,调用了wait,就会释放锁,此时,就会唤醒那些阻塞的线程,他们就会来争取锁)
  • WAITING线程线程会在Ower线程调用notify或notify all时唤醒,但唤醒后并不意味着立刻获得锁,仍需要进入队列,重新竞争。(也就是说,其他那些阻塞的线程拿到锁之后,可以调用notify或者notifyall,来唤醒前面调用了wait的线程)
    梳理一下逻辑:也就是说A线程拿到锁,锁对象调用了wait(),就会使A线程进入WaitSet集合,变成Waiting状态,并且会释放锁。此时就会唤醒那些阻塞的线程来竞争锁,就假如是B线程拿到了锁,他就可以进行线程运行了,此时,他可以调用notify()或者是notifyAll()来唤醒那些进入waiting状态的线程,来重新竞争锁

API介绍

  • obj.wait() 让进入 object 监视器(也就是Monitor,也就是拿到锁之后的执行区域)的线程到 waitSet 等待
  • obj.wait(long timeout) 也就是有等待的时间,时间过了还没被notify,我也被唤醒继续向下执行
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
也就是说同一把锁对象调用wait,他们就属于同一个waitset集合里面的waiting线程。同一把锁对象调用notify,也就是唤醒同一把锁的waitset里面的线程!!!

final static Object obj = new Object();
public static void main(String[] args) {
    new Thread(() -> {
        synchronized (obj) {
            log.debug("执行....");
            try {
                obj.wait(); // 让线程在obj上一直等待下去
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("其它代码....");
        }
    }).start();
    new Thread(() -> {
        synchronized (obj) {
            log.debug("执行....");
            try {
                obj.wait(); // 让线程在obj上一直等待下去
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("其它代码....");
        }
    }).start();
    // 主线程两秒后执行
    sleep(2);
    log.debug("唤醒 obj 上其它线程");
    synchronized (obj) {
        obj.notify(); // 唤醒obj上一个线程
        // obj.notifyAll(); // 唤醒obj上所有等待线程
    }
}

这一把锁,就包含了所有使用这个锁的所有线程(好好理解这句话)
注意:wait()之后被唤醒,会继续向下执行代码,就类似于sleep()到时间了一样

wait和notify正确使用姿势

开始之前先看看sleep(long n) 和 wait(long n) 的区别 (重中之重)

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法 (重中之重)
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要
    和 synchronized 一起用 (重中之重)
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 (重中之重)
  4. 但是他们有个共同点,就是状态都是TIME_WAITING状态,有时间的等待

将来对象锁我们都把它设置成static final,表示不可变,确保锁的都是同一个对象

例子1

这样好不好?

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        synchronized (room) {
            log.debug("有烟没?[{}]", hasCigarette);
            if (!hasCigarette) {
                log.debug("没烟,先歇会!");
                try {
                    sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            log.debug("有烟没?[{}]", hasCigarette);
            if (hasCigarette) {
                log.debug("可以开始干活了");
            }
        }
    }, "小南").start();
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("可以开始干活了");
            }
        }, "其它人").start();
    }
    sleep(1);
    new Thread(() -> {
        // 这里能不能加 synchronized (room)?
        //此时肯定不行咯。sleep都是不释放锁的
        //但是假如是wait就可以加锁,因为wait会释放锁
        hasCigarette = true;
        log.debug("烟到了噢!");
    }, "送烟的").start();

}

不好

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制
    改进
    没烟就使用对象锁wait(),送烟的使用对象锁来notifyall()。也就是改成room.wait()和room.notify()。再次说白了,也就是锁对象来控制线程之间的协作!!!而不是线程本身的Thread的方法

例子2

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        synchronized (room) {
            log.debug("有烟没?[{}]", hasCigarette);
            if (!hasCigarette) {
                log.debug("没烟,先歇会!");
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("有烟没?[{}]", hasCigarette);
            if (hasCigarette) {
                log.debug("可以开始干活了");
            } else {
                log.debug("没干成活...");
            }
        }
    }, "小南").start();
    new Thread(() -> {
        synchronized (room) {
            Thread thread = Thread.currentThread();
            log.debug("外卖送到没?[{}]", hasTakeout);
            if (!hasTakeout) {
                log.debug("没外卖,先歇会!");
                try {
                    room.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("外卖送到没?[{}]", hasTakeout);
            if (hasTakeout) {
                log.debug("可以开始干活了");
            } else {
                log.debug("没干成活...");
            }
        }
    }, "小女").start();
    sleep(1);
    new Thread(() -> {
        synchronized (room) {
            hasTakeout = true;
            log.debug("外卖到了噢!");
            room.notify();
        }
    }, "送外卖的").start();
}

好不好与改进
在这里插入图片描述
外卖送到了,唤醒的确实等烟的小南,这样错误的唤醒了。

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll(此时,两个线程都会被唤醒,小女有外卖,就可以干活了,但是小南没有烟,还是没干成活,继续等待。像这样,虽然解决了虚假唤醒的问题,但还是会有线程因为条件不满足,而继续执行不想要的逻辑。因为唤醒之后就会继续向下执行嘛,你想的是条件成立再唤醒,继续向下执行,而此时是条件不成立,也被唤醒继续向下执行了!!!)(重中之重)

进一步解决
问题:改为 notifyAll(此时,两个线程都会被唤醒,小女有外卖,就可以干活了,但是小南没有烟,还是没干成活,继续等待。像这样,虽然解决了虚假唤醒的问题,但还是会有线程因为条件不满足,而继续执行不想要的逻辑。因为唤醒之后就会继续向下执行嘛,你想的是条件成立再唤醒,继续向下执行,而此时是条件不成立,也被唤醒继续向下执行了!!!)
解决方法:把线程里面的wait()方法用while条件判断包裹,这样只有成立了,才会跳出wait()方法,如下:

while(条件){
	log.debug("条件不成立,继续等待")
	try{
		objLock.wait();
	}catch(Exception e){
		e.printStackTrace()
	}
}

这样他就会因为条件不成立,即便被唤醒,也会循环往复的wait()等待,直到被唤醒时,条件也成立了,才会跳出循环,继续向下执行,去执行条件成立后期望执行的代码。

这样就真正解决了虚假唤醒的问题,还是要理解,同一把锁的线程才在同一个waitset集合里面

总结wait与notify的正确使用姿势

synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    // 干活
}
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

同步模式之保护性暂停

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

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
  • JDK中,join的实现,Future的视线,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

在这里插入图片描述
实现
实现的关键在于中间的这个桥梁,“保护对象”

@Slf4j
public class JUCStudy {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            //等待结果
            log.debug("等待结果");
            guardedObject.getResponse();
        },"t1").start();
        new Thread(() -> {
            log.debug("执行下载");
            try {
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //假装下载了三秒后有了结果
            Object obj = new Object();
            guardedObject.setResponse(obj);
            log.debug("结果有了");
        },"t2").start();
    }
}
class GuardedObject {
    //结果
    private Object response;

    //获取结果的方法
    public Object getResponse() {
        synchronized (this) {
            //没有结果,就一直等待
            while(response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    //产生结果的方法
    public void setResponse(Object response) {
        synchronized (this) {
            //产生结果
            this.response = response;
            //有结果了唤醒那些等待的线程
            this.notifyAll();
        }
    }
}

在这里插入图片描述

上面可以看出,就是过了大约3秒,也就是下载完,有了结果之后,t1线程获得结果了,才继续执行。说白了,意思就是吧两个要配合的线程绑定在同一个对象里面,例如共享的资源,等待的结果啥的。然后两个线程之间就用这个对象锁来加锁。互相wait和nitifyAll,假如只有一个线程等待就可以用notify即可。因为他是唤醒同一个锁对象里的线程

对保护性暂停扩展—增加超时的效果

上面那个例子,假如下载的时间不是3秒,当网络不好时一直都在下载,那另一个等待结果的线程就会一直等待。就不好。此时,我们就有一个场景,线程1等待线程2的结果,我只想等一会,等超时了我就不等了,要怎么实现呢?

方法改进

//获取结果的方法(给方法加个参数---超时参数,传入之后可以选择最多等待多久)
public Object getResponse(long timeout) {
    synchronized (this) {
        //没有结果,就一直等待
        //开始时间
        long begin = System.currentTimeMillis();
        //经历时间
        long passedTime = 0;
        while(response == null) {
        	//这一轮循环应该等待的时间
            long waitTime = timeout - passedTime;//优化,可利用waitTime替换简化下面的参数,以及passedTime >= timeout===>waitTime <= 0
            //经历的时间,超过最大时间,就退出
            if (passedTime >= timeout) {
                break;
            }
            try {
                /*
                注意,这里也是还要加超时间的,因为本身就是需要这个wait()等待两秒先,再去通过开始时间经历时间来执行退出逻辑
                但是只写timeout,应对不了虚假唤醒的情况,假如过了一秒被虚假唤醒,还是没有结果,还是得循环,那下一次再等两秒,一共就是3秒了
                所以一开始等了1秒了,被唤醒之后下次就不需要等两秒,而是等待时间-已经等过的时间才对
                所以已经写timeout-passedTime
                 */
                this.wait(timeout - passedTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //求得经历时间,假如已经过了要等待的时间,就退出循环
            passedTime = System.currentTimeMillis() - begin;
        }
        return response;
    }
}

总结两点,第一点:不是单单的传入timeout时间参数进去wait()方法就可以了,因为他是一个while循环,目的是时间到了让他主动退出循环,所以需要的是一个开始等待时间和经过时间,来算出等了多久,跟timeout对比之后来判断时候否退出循环,继续执行后面的代码。第二点:为了应对虚假唤醒的情况,也不能单单只写一个timeout,被虚假唤醒,又没有结果,只能再次循环,因为下一次等并不需要等原来的时间,因为都已经等过了,所以应该是timeout-已经等过的时间才对

join原理

跟保护性暂停的不同,保护性暂停是一个线程等待另一个线程的结果
而join是一个线程等待另一个线程的结束

join带时间的源码,其实就跟上面写的代码很像
在这里插入图片描述
实际上join就是运用了保护性暂停模式

保护性暂停扩展—Future

图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

在这里插入图片描述

重点例子 (生产消费一对一)

用 Id 来标识 Guarded Object

@Slf4j
public class JUCStudy {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new People().start();
        }
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (Integer id : Mailboxes.getIds()) {
            new Postman(id,"内容"+id).start();
        }
    }
}

/**
 * 居民类
 */
@Slf4j(topic = "c.People")
class People extends Thread {
    @Override
    public void run() {
        //收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug("开始收信-id:{}",guardedObject.getId());
        Object mail = guardedObject.get(5000);
        log.debug("收到信-id:{},内容:{}",guardedObject.getId(),mail);

    }
}

/**
 * 邮递员类
 */
@Slf4j(topic = "c.Postman")
class Postman extends Thread {
    private int id;

    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        //送信
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.debug("送信-id:{},内容:{}",id,mail);
        guardedObject.complete(mail);
    }
}

/**
 * 中间解耦类
 */
class Mailboxes {
    //因为这个对象是会被多个线程访问到的,所以得用hashtable,目前只学了这个。这里就是多个有Id的小格子,在一个邮件架子上
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();


    private static int id = 1;


    // 产生唯一 id
    private static synchronized int generateId() {
        return id++;
    }


    //根据ID获取 Object (获取后应该把邮件删除,注意:get是根据键返回值,而remove是根据键返回值,同时还把这个键值对删除掉,真牛,刚好符合业务逻辑,不用获取了保存、再删除、再返回)
    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    //创建GuardedObject
    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    //返回所有对象
    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}




class GuardedObject {

    // 标识 Guarded Object
    private int id;


    public GuardedObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }


    // 结果
    private Object response;
    // 获取结果
    // timeout 表示要等待多久 2000
    public Object get(long timeout) {
        synchronized (this) {
            // 开始时间 15:00:00
            long begin = System.currentTimeMillis();
            // 经历的时间
            long passedTime = 0;
            while (response == null) {
                // 这一轮循环应该等待的时间
                long waitTime = timeout - passedTime;
                // 经历的时间超过了最大等待时间时,退出循环
                if (timeout - passedTime <= 0) {
                    break;
                }
                try {
                    this.wait(waitTime); // 虚假唤醒 15:00:01
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
            }
            return response;
        }
    }
    // 产生结果
    public void complete(Object response) {
        synchronized (this) {
            // 给结果成员变量赋值
            this.response = response;
            this.notifyAll();
        }
    }
}

结果:
在这里插入图片描述

解释
首先,上面的Mailboxs和GuardedObject其实是可以复用的,可以起一个通用点的名字,在项目中可以直接拿来用。跟业务相关的是邮递员和居民

结果产生者和结果的消费者是一一对应的关系

也就是一个邮递员送一个信给居民,而不是一个邮递员承包所有信件

private static Map<Integer, GuardedObject> boxes = new Hashtable<>();

我觉得主要是还是上面这一行代码,用线程安全的HashTable来装id和保护对象类
在这里插入图片描述
其实,中间那个Futures,也就是所说的解耦类,里面其实就是对保护对象的装、增、删、获取的操作。也就是个盒子,装了很多有对应id的保护对象。然后产生结果的,就根据所有id来产生所有结果。然后要等待结果的,就根据id来等待,直到产生结果的产生完。不知道这样能不能理解

邮递员获取一个个信件,根据id来送信。收信的根据id,来等待信件的送来。

异步模式之生产者/消费者

要点:

  • 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以采用平衡生产和消费的线程资源
  • 生产者仅负责生产结果数据,不关数据后续该如何处理,而消费者专心处理数据结果,不关心数据从何而来
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会在消耗数据(说白了满了就不会再从生产者获取,空的时候不会在推送消息出去给消费者)
  • JDK中各种阻塞队列,采用的就是这种模式

在这里插入图片描述
例子 重点

@Slf4j(topic = "c.study")
public class JUCStudy {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);


        for (int i = 0; i < 3; i++) {
            int id=i;
            new Thread(() -> {
                queue.put(new Message(id,"值"+id));
            },"生产者"+i).start();
        }

        new Thread(() -> {
            while (true) {
                //隔一秒消费一个消息
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                queue.take();
            }
        },"消费者").start();
    }
}
@Slf4j(topic = "c.MessageQueue")
//消息队列类,Java线程之间的通信
class  MessageQueue{

    //消息队列集合,双向链表比较合适
    private LinkedList<Message> list = new LinkedList<>();

    //队列容量
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    //获取消息
    public Message take() {
        //先检查队列是否为空
        synchronized (list){
            while (list.isEmpty()){
                try {
                    log.debug("队列空了,消费者线程进入等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列的头部获取元素(消息)返回
            Message message = list.removeFirst();
            log.debug("已消费消息{}",message);
            list.notifyAll();
            return message;
        }

    }

    //存入消息
    public void put(Message message){
        synchronized (list){
            //检查队列是否满了
            while (list.size() == capacity){
                try {
                    log.debug("队列满了,生产者线程进入等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列尾部插入元素
            list.addLast(message);
            log.debug("已生产消息{}",message);
            list.notifyAll();
        }

    }

}

@Data
@NoArgsConstructor
@AllArgsConstructor
final class Message{
    private int id;

    private Object value;

}

运行结果:
在这里插入图片描述
如上图所示:生产了三条消息,也消费了三条消息,注意,进程还是一直在运行的,因为消费者一直在循环消费,消费完了,进入等待状态,等待生产者生产。代码的画,很容易理解,主要注意的点就是加锁,和对象锁的notifyAll与wait的正确搭配使用

Park和Unpark方法

基本使用
他们是LockSupport类中的方法

//暂停当前线程
LockSupport.park();//该线程进入WAITING状态

//恢复某个线程的运行
LockSupport.unpark(线程对象);

先park再unpark

@Slf4j(topic = "c.study")
public class JUCStudy {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();


        try {
            sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

运行结果:
在这里插入图片描述
例子也很简单,主线程unpark了t1线程。值得注意的是park之后是WAITING状态

前面其实也有讲过,unpark之后,线程再执行park是无效的,因为unpark之后会把打断标记置为true,park对被打断过的线程是无效的。我记得前面应该是这样讲的

特点
与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sTr1ve.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值