线程间等待与唤醒机制、单例模式、阻塞队列、定时器

目录

线程间等待与唤醒机制

线程等待wait

唤醒方法notify

面试题:wait方法和sleep方法的区别

练习

单例模式 

饿汉式单例

懒汉式单例

解决懒汉式的线程安全问题

阻塞式队列

JDK中的阻塞队列BlockingQueue

定时器——类比现实生活中的闹钟


线程间等待与唤醒机制

线程间等待与唤醒机制。wait和notify是Object类的方法,用于线程的等待与唤醒。无论是wait还是notify方法,都需要搭配synchronized锁来使用(等待和唤醒,也是需要对象)。

线程等待wait

多线程并发的场景下,有时需要某些线程先执行,这些线程执行结束后其他线程再继续执行。

死等,线程进入阻塞态(WAITING)直到有其他线程调用notify方法唤醒。

等待一段时间,若在该时间内线程被唤醒,则继续执行;

若超过相应时间还没其他线程唤醒此线程,此线程就不再等待,恢复执行。

唤醒方法notify

唤醒方法
notify():随机唤醒一个处在等待状态的线程
notifyAll():唤醒所有处在等待状态的线程

    private static class notifyTask implements Runnable {
        private Object lock;

        public notifyTask(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("准备唤醒");
                // 唤醒所有处在等待状态的线程
                lock.notifyAll();
                System.out.println("唤醒结束");
            }
        }
    }
public class waitDemo1 {
    private static class waitTask implements Runnable {
        private Object lock;

        public waitTask(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("等待结束,本线程继续执行");
            }
        }
    }

    private static class notifyTask implements Runnable {
        private Object lock;

        public notifyTask(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("准备唤醒");
                lock.notify();
                System.out.println("唤醒结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(new waitTask(lock), "t1");
        Thread t2 = new Thread(new waitTask(lock), "t2");
        Thread t3 = new Thread(new waitTask(lock), "t3");
        Thread notify = new Thread(new notifyTask(lock), "notify线程");
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(100);
        notify.start();
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第1张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第2张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第3张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第4张图片

对于wait和notify方法,其实有一个阻塞队列和一个等待队列
阻塞队列表示同一时间只有一个线程能获取到锁,其他线程进入阻塞队列。

等待队列:表示线程调用wait(首先此线程要获取到锁,才能进入等待队列,最后释放锁),

调用wait方法的线程就会进入Waiting状态,等待被其他线程唤醒(lock.notify())。

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第5张图片

面试题:wait方法和sleep方法的区别

a. 若这两个方法有联系,就先答共性,再答区别;
b. 若这两方法毫无关系,就分别介绍即可。

作答:
1. wait方法是Object类提供的方法,需要搭配synchronized锁来使用,调用wait方法会释放锁,线程进入WAITING状态,等待被其他线程唤醒或者超时自动唤醒,唤醒之后的线程需要再次竞争synchronized锁才能继续执行。
2. sleep方法是Thread类提供的方法,调用sleep方法的线程进入TIMED_WAITING状态,不会释放锁,时间到自动唤醒

练习

求输出

public class waitDemo1 {
    private static class waitTask implements Runnable {
        private Object lock;

        public waitTask(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
                // 此线程在等待lock对象的notify方法唤醒
                try {
                    lock.wait();
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
            }
        }
    }

    private static class notifyTask implements Runnable {
        private Object lock;

        public notifyTask(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("准备唤醒");
                lock.notifyAll();
                System.out.println("唤醒结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(new waitTask(lock), "t1");
        Thread t2 = new Thread(new waitTask(lock2), "t2");
        Thread t3 = new Thread(new waitTask(lock2), "t3");
        Thread notify = new Thread(new notifyTask(lock2), "notify线程");
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(100);
        notify.start();
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第6张图片

单例模式 

单例模式:校招中考察频率非常高的一个设计模式(共23种设计模式–编程思想,不同场景下该如何设计和实现代码的固定套路)
所谓的单例模式保证某个类在程序中有且只有一个对象。
现实生活中的单例:一个类只有一个对象,地球类-只有地球这一个对象,太阳类-只有太阳这一个对象。


如何控制某个类只有一个对象呢?

1. 要创建类的对象,通过构造方法产生对象;

2. 构造方法若是public权限,对于类的外部,随意创建对象,无法控制对象的个数;
3. 将构造方法私有化,类的外部彻底没法产生对象,一个对象都没有。

public class SingleTon {
    private SingleTon() {

    }
}

系统默认的无参没了,对于SingleTon的外部就彻底没法产生SingleTon的对象了。


构造方法私有化之后,对于类的外部而言就一个对象都没有。
如何构造这唯一的对象(私有化的构造方法只能在类的内部调用),只调用一次构造方法即可。

public class SingleTon {
    private SingleTon singleTon = new SingleTon();

    private SingleTon() {

    }
}

问题:此时这个唯一变量使用成员变量是否可行?

X,类中的成员变量必须通过对象访问,对于SingleTon的外部压根就没对象,无法通过对象访问。

类的外部就是要获取这个唯一的对象,才能访问,现在外部没有对象,没办法通过对象访问。

在SingleTon类的外部访问这个唯一的对象
直接通过getSingleTon方法获取这个唯一的对象

public class SingleTon {
    // 唯一的这一个对象
    private static SingleTon singleTon = new SingleTon();

    private SingleTon() {

    }
    // 调用此方法时,singleTon已经产生了
    public static SingleTon getSingleTon() {
        return singleTon;
    }
}

单例模式三步走
1. 构造私有化(保证对象的产生个数);
2. 单例类的内部提供这个唯一的对象(static);
3. 单例类提供返回这个唯一的对象的静态方法供外部使用。

饿汉式单例

天然线程安全
系统初始化JVM加载类的过程中就创建了这个唯一的对象

/**
 * 饿汉式单例。(类加载就产生这个唯一对象)饥不择食,这个类一加载就把唯一的这个对象产生了。
 * 我也不管外部到底用不用这个对象,只要这个类加载到JVM,唯一对象就会产生
 */
public class SingleTon {
    private static SingleTon singleTon = new SingleTon();

    private SingleTon() {

    }

    public static SingleTon getSingleTon() {
        return singleTon;
    }
}

package thread.single;
public class Main {
    public static void main(String[] args) {
        SingleTon s1 = SingleTon.getSingleTon();
        SingleTon s2 = SingleTon.getSingleTon();
        SingleTon s3 = SingleTon.getSingleTon();
        System.out.println(s1 == s1);
        System.out.println(s1 == s3);
    }
}

懒汉式单例

只有第一次调用getSingleTon方法,表示外部需要获取这个单例对象时才产生对象

未优化代码

/**
 * 懒汉式单例
 */
public class LazySingleTon {
    private static LazySingleTon singleTon;

    private LazySingleTon() {

    }

    // 第一次调用获取单例对象方法时才实例化对象
    public static LazySingleTon getSingleTon() {
        if (singleTon == null) {
            singleTon = new LazySingleTon();
        }
        return singleTon;
    }
}

系统初始化时,外部不需要这个单例对象,就先不产生,只有当外部需要此对象才实例化对象。这种操作称之为懒加载

例如:HashMap中

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第7张图片

懒加载,只有需要给map中添加元素时,表示此时需要table数组,才初始化数组大小为16。


问题:多线程场景下是否能确保只有一个对象产生了?

答:饿汉模式可以,懒汉模式下不能。

饿汉

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第8张图片

 懒汉

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第9张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第10张图片

解决懒汉式的线程安全问题

1. 最简单粗暴的方式,直接在静态方法上加锁

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第11张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第12张图片


优化刚才的方法锁

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第13张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第14张图片

当t1先进入同步代码块之后,t2和t3卡在获取锁的位置,t1产生对象后,锁释放;
t2和t3还是从获取锁的位置继续执行,t2和t3就会再次new对象

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第15张图片


double-check优化

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第16张图片

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第17张图片

在同步代码块内部需要再次检查singleTon是否为空,防止其他线程恢复执行后多次创建单例对象。

问题:不使用double-check,直接把if写道synchronized里面不应该更简单一点吗?

答:这个单例只是最核心的代码,单例模式还有很多其他操作,为了保证其他操作尽可能的并发执行。

双重加锁,使用volatile关键字保证单例对象的初始化不被中断

/**
 * 懒汉式单例
 */
public class LazySingleTon {
    private static volatile LazySingleTon singleTon;

    private LazySingleTon() {
        int x = 10;
        int y = 20;
        int z = 30;
    }

    public  static LazySingleTon getSingleTon() {
        if (singleTon == null) {
            synchronized (LazySingleTon.class) {
                if (singleTon == null) {
                    singleTon = new LazySingleTon();
                }
            }
        }
        return singleTon;
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第18张图片

请写出单例模式
1. 如果你对double - check的理解到位了,直接写懒汉式的double - check。

2. 如果感觉稍微有点慌,就写个饿汉。

阻塞式队列

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第19张图片

和普通队列最大的区别在于入队和出队会阻塞:
入队时,若队列已满,则入队操作会"阻塞",直到有其他线程从队列中取出元素;

出队时,若队列为空,则出队操作会"阻塞",直到有其他线程向队列中添加元素。

生产者消费者模型

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第20张图片

例如拍卖、秒杀场景——流量削峰

10w个用户等待拍卖,此时将支付请求交给队列,服务器不直接处理支付逻辑,有专门处理支付逻辑的程序从队列中取出请求依次处理。

JDK中的阻塞队列BlockingQueue

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第21张图片

入队方法put()阻塞式入队方法,出队方法take()阻塞式的出队。

通过 锁 + wait 和notify 机制实现阻塞队列。

常用子类
ArrayBlockingQueue

LinkedBlockingQueue

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new LinkedBlockingDeque<>();
        blockingQueue.put(1);
        System.out.println(blockingQueue.take());
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new LinkedBlockingDeque<>();
        // 当阻塞队列为空,take方法就会阻塞
        System.out.println(blockingQueue.take());
        blockingQueue.put(1);
    }
}

take方法一直处在阻塞中。


import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new LinkedBlockingDeque<>(3);
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    // 当阻塞队列为空,take方法就会阻塞
                    int val = blockingQueue.take();
                    System.out.println("消费元素:" + val);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "消费者");
        Random random = new Random();
        Thread producer = new Thread(() -> {
            while (true) {
                try {
                    int val = random.nextInt(100);
                    // 当队列已满,put方法就会阻塞
                    blockingQueue.put( val);
                    System.out.println("生产元素:" + val);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "生产者");
        customer.start();
        producer.start();
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第22张图片


阻塞队列的大小一般通过构造方法传入,没有参数就是无界队列

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new LinkedBlockingDeque<>(3);
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    // 当阻塞队列为空,take方法就会阻塞
                    int val = blockingQueue.take();
                    System.out.println("消费元素:" + val);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "消费者");
        Random random = new Random();
        Thread producer = new Thread(() -> {
            while (true) {
                try {
                    int val = random.nextInt(100);
                    // 当队列已满,put方法就会阻塞
                    blockingQueue.put( val);
                    System.out.println("生产元素:" + val);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "生产者");
//        customer.start();
        producer.start();
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第23张图片

定时器——类比现实生活中的闹钟

设定一个时间以及一个相应的任务

如:三分钟后播放电影

在web编程部分,检测客户端的连接,500ms之后没有收到数据,断开连接;
LRU缓存希望某个键值对3s之后就过期(删除)。


JDK中使用Timer类描述定时器

核心方法就是schedule方法,两个参数(指定时间到了要执行的任务,等待时间- ms)。

1. 延迟3s之后执行TimerTask任务

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 3s之后执行此任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000);
    }
}

 

2. 延迟3s之后开始执行任务,该任务启动之后每隔1s就会再次执行

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 3s之后执行此任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000,1000);
    }
}

线程间等待与唤醒机制、单例模式、阻塞队列、定时器_第24张图片

单位都是ms,参数:(要执行的任务,延迟多久开始执行,每隔多久执行一次)。

你可能感兴趣的:(JavaEE,java-ee,java)