目录
线程间等待与唤醒机制
线程等待wait
唤醒方法notify
面试题:wait方法和sleep方法的区别
练习
单例模式
饿汉式单例
懒汉式单例
解决懒汉式的线程安全问题
阻塞式队列
JDK中的阻塞队列BlockingQueue
定时器——类比现实生活中的闹钟
线程间等待与唤醒机制。wait和notify是Object类的方法,用于线程的等待与唤醒。无论是wait还是notify方法,都需要搭配synchronized锁来使用(等待和唤醒,也是需要对象)。
多线程并发的场景下,有时需要某些线程先执行,这些线程执行结束后其他线程再继续执行。
死等,线程进入阻塞态(WAITING)直到有其他线程调用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(); } }
对于wait和notify方法,其实有一个阻塞队列和一个等待队列
阻塞队列表示同一时间只有一个线程能获取到锁,其他线程进入阻塞队列。等待队列:表示线程调用wait(首先此线程要获取到锁,才能进入等待队列,最后释放锁),
调用wait方法的线程就会进入Waiting状态,等待被其他线程唤醒(lock.notify())。
面试题: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(); } }
单例模式:校招中考察频率非常高的一个设计模式(共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中
懒加载,只有需要给map中添加元素时,表示此时需要table数组,才初始化数组大小为16。
问题:多线程场景下是否能确保只有一个对象产生了?
答:饿汉模式可以,懒汉模式下不能。
饿汉
懒汉
1. 最简单粗暴的方式,直接在静态方法上加锁
优化刚才的方法锁
当t1先进入同步代码块之后,t2和t3卡在获取锁的位置,t1产生对象后,锁释放;
t2和t3还是从获取锁的位置继续执行,t2和t3就会再次new对象
double-check优化
在同步代码块内部需要再次检查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; } }
请写出单例模式
1. 如果你对double - check的理解到位了,直接写懒汉式的double - check。2. 如果感觉稍微有点慌,就写个饿汉。
和普通队列最大的区别在于入队和出队会阻塞:
入队时,若队列已满,则入队操作会"阻塞",直到有其他线程从队列中取出元素;出队时,若队列为空,则出队操作会"阻塞",直到有其他线程向队列中添加元素。
生产者消费者模型
例如拍卖、秒杀场景——流量削峰
10w个用户等待拍卖,此时将支付请求交给队列,服务器不直接处理支付逻辑,有专门处理支付逻辑的程序从队列中取出请求依次处理。
入队方法put()阻塞式入队方法,出队方法take()阻塞式的出队。
通过 锁 + wait 和notify 机制实现阻塞队列。
常用子类
ArrayBlockingQueueLinkedBlockingQueue
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(); } }
阻塞队列的大小一般通过构造方法传入,没有参数就是无界队列
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(); } }
设定一个时间以及一个相应的任务
如:三分钟后播放电影
在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); } }
单位都是ms,参数:(要执行的任务,延迟多久开始执行,每隔多久执行一次)。