线程通信
什么是线程通信?使用场景?
多线程优势是提高cpu利用率(通过并发并行)执行时间比较的长任务,可能存在线程安全问题,且线程之间抢占式执行,执行顺序难以预知(随机执行)。
如何让线程具有一定的顺序性,在保证安全和效率前提下?
线程间通信:一个线程以通知的方式,唤醒某些等待线程或在某些条件下让当前线程等待,通过这样让线程间通过通信方式满足一定顺序性。
方法:
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll(): 唤醒在当前对象上等待的线程
wait,notify,notifyAll 都是Object类方法
Wait()方法
wait 执行:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify()方法
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其 它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。(在同步块/方法中执行)
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")(随机唤醒一个线程)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。(执行完才会释放对象锁)
notifyAll()方法
notifyAll()方法唤醒所有等待线程
唤醒的线程(处于等待状态的),及调用synchronized竞争锁失败而阻塞的线程,再次进程锁,但还是只有一个竞争成功
使用方式及案例
方式:
- 同步块,同步方法中
需要在synchronized结束(当前线程释放锁)后,以通知的方式,唤醒之前调用wait等待的线程
- notify随机唤醒一个
- notifyAll 全部唤醒
简单使用
package 线程通信;
public class 简单使用 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock) {
//先做一些事情
System.out.println("线程1:步骤1");
//在某些条件下,就需要等待
lock.wait();
//被唤醒,就做另一些事情
System.out.println("线程1:步骤2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
/**
* 执行顺序:
* 线程1 synchronized -> wait
* 线程2 synchronized -> notify
* 线程1 wait 往下
*/
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
//做一些事情
System.out.println("线程2:步骤1");
//通知线程1:唤醒执行
lock.notify();
//做另一些事情
System.out.println("线程2:步骤2");
}
}
}).start();
}
}
多线程案例
单例模式
常考设计模式之一:设计模式面对一些问题场景的固定套路
概念:
保证某个类在程序中只存在唯一一份实例,(不会创建多个实例)
很多场景需要,例如JDBC中的DataSource,数据库连接池。
具体实现方式分两种:饿汉和懒汉
饿汉式
在类加载时,创建实例:利用static特性(类加载时,且只执行一次)
写法:静态变量初始化时赋值实例化
class Singleton{
//类加载时创建
private static Singleton instance = new Singleton();
//构造函数
private Singleton(){}
//唯一实例,获取接口
public static Singleton getInstance(){
return instance;
}
}
注意:即使不使用,也会创建对象(占用内存空间)
懒汉式
写法:初始化时不赋值,使用时,如果没有初始化再初始化
单线程
class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance==null)
instance = new Singleton();
return instance;
}
}
但再多线程模式下不安全,多个线程同时调用getInstance方法会创建出多个实例!!
多线程重点
//单例读多,写少,读volatile,写加锁
class Singleton{
//instance:读操作,加volatile 保证安全性
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//一层读直接返回;若为写,加锁
if(instance==null){
//写操作,加锁
synchronized(Singleton.class){
//防止多线程,再次创建改变引用
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
阻塞队列
阻塞队列(同样遵循“先进先出”)能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素
典型应用:生产者消费者模型(经典的开发模型)
生产者消费者模型
通过一个容器解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,通过阻塞队列进行通讯;
生产者生产完数据后,直接给阻塞队列;消费者再阻塞队列中取数据;
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.(削峰)
防止生产过快
- 阻塞队列使生产者和消费者解耦(减弱依赖关系)
标准库中的阻塞队列BlockingQueue
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
线程池
池:字符串常量池,数据库连接池等(类似缓存)
理解(暂时):初始化(new线程池)的时候,创建一定数量的线程(不停的在线程池内部的一个阻塞队列)取任务(消费者)。
我们可以在其他线程中提交任务(生产者)至线程池
优势:线程的创建及销毁存在一定代价(性能消耗),使用线程池可以重复使用线程执行多组任务。
jdk原生的线程池api
ThreadPoolExecutor( //即使空闲时仍保留在池中的线程数(正式工),除非设置 allowCoreThreadTimeOut
int corePooSize,
//池中允许的最大线程数(正式工+临时工)
int maximumPoolSize,
//当线程数大于内核(正式工)时,这是多余的空闲线程在终止前等待新任务的最大时间。
long keepAliveTime,
//keepaliveTime的时间单位,是秒,分钟,或其他值
TimeUnit unit,
//传递任务的阻塞队列,这个队列将仅保存execute方法提交的Runnable任务
BlockingQueue<Runnable> workQueue,
//执行程序创建新线程时使用的工厂
ThreadFactory threadFactory,
//拒绝策略,(工作任务超出负荷,如何处理)
RejectedExecutionHandler handler)
//creates a new ThreadPoolExecutor with the given inital parameters
拒绝策略:(一般需要自己实现拒绝策略)
- AbortPolicy():超出负荷,直接抛出异常(默认的拒绝策略)
- CallerRunsPolicy():调用者负责处理(那个线程提交的任务,那个线程去执行)
- DiscardOldestPolicy():丢弃队列中最老的任务
- DiscardPolicy():丢弃新来的任务
线程池使用:
- new线程池
- 提交任务
- 线程池对象.execute(Runnable task)
- 线程池对象.submit(Runnable tack)