一、单例模式
单例模式能保证某个类在程序中只存在 唯一一份实例 ,而不会创建出多个实例。
-
饿汉式 : 直接就将对象创建出来,不管后面会不会用到,需要时就直接返回。因为在类加载时就创建好对象了,后续不需要再创建,相当于只有读操作,所以是 线程安全 的
// 通过 Singleton 这个类来实现 class Singleton{ // 这个 instance 就是该类的唯一实例 private static Singleton instance = new Singleton();// 类成员 // 为了防止再获得他的对象 (new),将构造方法私有化 private Singleton(){ } // 提供方法让外界拿到 instance 对象 public static Singleton getInstance(){ return instance; } }
-
懒汉式: 当需要用到时,再创建实例,后续不再创建
// 通过 Singleton 这个类来实现 class Singleton{ // 这个 instance 就是该类的唯一实例 private static volatile Singleton2 instance = null;// 类成员 // 为了防止再获得他的对象,将构造方法私有化 private Singleton(){ } // 提供方法让外界拿到 instance 对象 // 只有当真正要用到实例时,才会创建实例 public static Singleton getInstance(){ // 判断是否已经创建过对象了 if (instance == null) { instance = new Singleton(); } return instance; } }
但是我们可以发现在懒汉式中, 该方法有读,还有修改对象,而且不是原子的,线程不安全。
如何实现一个线程安全的单例模式?
-
加锁(synchronized),将读写操作封装成一个原子操作
synchronized (Singleton.class){ // 看是否要创建实例 if (instance == null) { // 初始化之后,就不会再进行修改,线程安全 instance = new Singleton(); } }
-
这样虽然没问题,但是频繁的获取和释放锁对象,会造成很大的开销,所以在外层加上一层判定,如果已经创建好了实例,就不去竞争锁,直接返回。
// 看要不要竞争锁 if (instance == null) { synchronized (Singleton.class){ // 看是否要创建实例 if (instance == null) { instance = new Singleton(); } } }
-
当多个线程同时读 instance ,会有内存可见性问题,当还没有创建好对象时,多个读到的都是 null ,即使多个线程中有一个线程创建了instance 实例,其他线程还是会去竞争锁对象。所以加上 volatile 关键字,避免无用的锁竞争。
private static volatile Singleton instance = null;
完整代码:
// 通过 Singleton 这个类来实现
class Singleton{
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;
}
}
二、阻塞队列
阻塞队列是一种特殊的队列,除了具有队列的性质外,还具有阻塞的功能。例如下面的典型案例 – 生产者消费者模型
- 当队列满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素
- 当队列空时,继续出队列也会阻塞,直到有其他线程往队列中插入元素
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
比如 “双十一” 当天,淘宝的服务器同一时刻可能会收到大量的支付请求。如果直接处理这些支付请求,可能扛不住。就将这些请求放到一个阻塞队列中,然后再由消费者线程慢慢来处理每个请求。这样做可以有效进行 “削峰”,防止服务器被突然到来的一波请求直接冲垮。 - 阻塞队列也能使生产者和消费者之间 解耦 (因为有一个缓冲区,生产者和消费者不需要有直接联系,通过缓冲区即可实现)
例如买手机,消费者不需要知道是谁生产的手机,生产者也不需要知道是谁买手机。两者没有直接的联系,而是通过手机店这样的类似与中介的地方进行交易,一定程度上解除了耦合。
java标准库中的阻塞队列(BlockingDeque)
- BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue
- put 方法用于阻塞式的入队列,take 用于阻塞式的出队列
- BlockingQueue 也有 offer,poll,peek 等方法,但是这些方法不带有阻塞特性
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.put("hello");// 入队列
String s = queue.take();// 出队列
实现一个阻塞队列: 普通循环队列 + 线程安全( synchronized) + 阻塞功能( wait、notify)
- 当队列满时,put 操作阻塞等待(wait),直到被 take 的操作(notify)唤醒
- 当队列空时,take 操作阻塞等待(wait),直到被 put 的操作(notify)唤醒
// 实现一个阻塞队列
class MyBlockingQueue{
// 数组 -- 循环队列
private int[] data = new int[1000];
private int size = 0;// 元素个数
private int head;// 队首下标
private int tail;// 队尾下标
// 入队列
// 每个都在操作公共变量,就给整个方法加锁
public synchronized void put(int val) throws InterruptedException {
if (size == data.length) {
// 队满,阻塞
wait();
}
data[tail++] = val;
// tail 达到末尾
if (tail >= data.length) {
tail = 0;
}
size++;
// 入队列成功,队列非空,唤醒 take()
// 如果take()处于阻塞态,就能唤醒,不处于阻塞态,也没有副作用
notify();
}
// 出队列
public synchronized Integer take() throws InterruptedException {
if (size == 0) {
wait();
}
int val = data[head];
head++;
if (head == data.length) {
head = 0;
}
size--;
// take成功之后,队列非满,唤醒 put的等待
notify();
return val;
}
}
基于上述阻塞队列,实现一个简单的生产者消费者模型
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
int num = 0;
while (true) {
try {
System.out.println("生产了 " + num);
queue.put(num);
num++;
// 生产慢
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
// 消费者线程
Thread customer = new Thread(() -> {
while (true) {
try {
// 消费慢
//Thread.sleep(500);
int num = queue.take();
System.out.println("消费了 " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
三、定时器
什么是定时器
定时器也是软件开发中的一个重要组件,类似于一个 “闹钟”,达到一个设定的时间之后,就执行某个指定的代码
-
标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule
-
schedule 包含两个参数,第一个参数指定要执行的任务,第二个参数指定多长时间之后执行 (毫秒)
Timer timer = new Timer(); // new TimerTask()表示一个任务 timer.schedule(new TimerTask(){ @Override public void run() { System.out.println("我是要执行的任务"); } },3000);// 3000ms后执行该任务
Timer 类的实现:
-
管理很多任务
1.描述一个任务(创建一个类 MyTask)// 表示一个任务 class MyTask{ // 任务具体要干啥 private Runnable runnable; // 任务什么时候干 private long time; // delay 是一个时间间隔,不是绝对的时间 public MyTask(Runnable runnable,long delay) { this.runnable = runnable; this.time = System.currentTimeMillis() + delay; } public void run(){ // 通过这个方法,执行任务 runnable.run(); } public long getTime(){ return this.time; } }
2.组织一个任务 (数据结构)
- 假设有很多任务,10min后写作业,20min后打游戏,200min后出去玩。我们应该用什么数据结构组织能按时间顺序拿到任务呢?
- 按照时间先后执行 – 优先级队列(堆)
因为堆要进行比较,所以放入的元素如果是自定义类型,要指定比较方式,所以让MyTask类实现Comparable接口,并重写compareTo()方法指定比较方式(按时间顺序排序)
// 表示一个任务 class MyTask implements Comparable<MyTask>{ // ...... 前面一样 // 重写compareTo方法,指定比较方式 @Override public int compareTo(MyTask o) { return (int)(this.time - o.time); } }
-
在MyTimer类中,使用一个 带有阻塞功能的优先级队列 来放入任务
class MyTimer{ // 带有阻塞功能的优先级队列 -- 要考虑线程安全问题,可能在多个线程进行注册任务,同时还有线程来执行 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(); }
-
schedule方法 – 按照传入的参数,封装一个MyTask 对象,放入优先级队列中
// 把任务放进队列 public void schedule(Runnable runnable,long delay){ MyTask task = new MyTask(runnable, delay); queue.put(task); }
-
执行时间到了的任务 – 需要有一个线程去扫描,看是否有任务到了执行时间
但是这里不能让该线程一直去扫描,例如“3:50”该做作业,从“3:30”开始,就一直拿手机看时间是否到了,这显然是不科学的,属于“忙等”。
正确做法应该是:看排在最前面的任务,执行时到到没,没到则等待该任务的对应时间wait(time),然后再看是否到了,相当于“定闹钟”操作
private Object locker = new Object();// 一个锁对象 // 需要执行最靠前的任务 // 需要有一个线程来检查 小根堆的顶元素(任务),看是否需要执行了 public MyTimer(){ Thread t = new Thread(() -> { while (true){ try { // 取出堆顶任务 MyTask task = queue.take(); // 看是否到执行时间了 long curTime = System.currentTimeMillis(); // 如果时间还没到 if (curTime < task.getTime()){ // 将任务塞回优先级队列 queue.put(task); // 等待相应时间 wait() synchronized (locker){ locker.wait(task.getTime() - System.currentTimeMillis()); } }else { // 时间到了 --- 执行该任务 task.run(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); }
-
这里又出现了一个问题:假设本来有一个阻塞队列,现在插入一个新的任务task4(5min)
可以看到,新任务的执行时间更靠前,所以需要唤醒一下线程,让他再扫描一下堆顶元素,看是否到执行时间了,所以在schedule方法中,需要执行唤醒操作
public void schedule(Runnable runnable,long delay){
MyTask task = new MyTask(runnable, delay);
queue.put(task);
// 每次任务插入成功后,唤醒一下线程,让他检查一下堆顶元素是否到执行时间了
synchronized (locker){
locker.notify();
}
}
整体代码
// 描述一个任务
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
private long time;
public MyTask(Runnable runnable,long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return this.time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
// 定时器类
class MyTimer{
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask task = new MyTask(runnable, delay);
queue.put(task);
synchronized (locker){
locker.notify();
}
}
private Object locker = new Object();
public MyTimer(){
Thread t = new Thread(() -> {
while (true){
try {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()){
queue.put(task);
synchronized (locker){
locker.wait(task.getTime() - System.currentTimeMillis());
}
}else {
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
四、线程池
简单来说线程池就是,把线程创建好,放在池子里。线程用完了,不是还给系统,而是放回池子里,以备下一次用。后面需要用线程时,不必从系统这边申请,而是直接从池子里拿,一定程度上减少了开销。
标准库中线程池
public static void main(String[] args) throws InterruptedException {
// 创建一个固定线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
// 创建一个自动扩容的线程池
// Executors.newCachedThreadPool();
// 创建一个只有一个线程的线程池
// Executors.newSingleThreadExecutor();
// 创建一个带有定时器功能的线程池
// Executors.newScheduledThreadPool();
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadPoll");
}
});
}
}
实现一个线程池
-
描述一个任务(Runnable)
-
组织任务(BlockingQueue)
private static BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
-
描述工作线程
// 描述一个工作线程,工作线程的功能就是从队列中取任务来执行 static class Worker extends Thread{ @Override public void run() { // 拿到上面的队列,取出任务执行 while (true) { try { // 循环获取任务 // 如果队列空,则会阻塞 Runnable runnable = MyThreadPool.queue.take(); runnable.run();// 执行该任务 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
组织工作线程
// 创建一个数据结构来组织线程 private List<Thread> workers = new ArrayList<>(); public MyThreadPool(int n) { // 创建若干个线程,放到上述数组 for (int i = 0; i < n; i++) { Worker worker = new Worker(); worker.start(); workers.add(worker); } }
-
需要实现往线程池添加任务
// 创建一个方法,允许程序员放任务到线程池 public void submit(Runnable runnable){ try { queue.put(runnable); } catch (InterruptedException e) { e.printStackTrace(); } }
整体代码
class MyThreadPool {
private static BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
static class Worker extends Thread{
@Override
public void run() {
while (true){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private List<Thread> workers = new ArrayList<>();
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Worker worker = new Worker();
worker.start();
workers.add(worker);
}
}
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}