单例模式
首先我们需要知道什么是设计模式?
设计模式就像“棋谱”,针对常见的问题场景,大佬们总结的固定套路。
那么单例模式是什么?
单例模式就是限制类只有唯一的实例。
下面我们以洗碗这个情景来讲述饿汉模式和懒汉模式
饿汉模式
饿汉模式就相当于洗碗池里有一堆碗,而你此时要吃饭,你就一股脑的把所有碗都洗了,用的时候全部洗完。
饿汉模式:使用static在类加载阶段创建实例
private Singleton() {
}
private static Singleton instance=new Singleton();
public static Singleton getInstance() {
return instance;
}
当多个线程调用getInstance时,是多个线程读,不涉及修改线程安全。
懒汉模式
懒汉模式就相当于你要吃饭,洗碗时只洗一个吃饭的碗,用的时候再洗,用多少洗多少。
懒汉模式:通过getInstance方法获取实例。首次调用该方法,才真正创建实例。(延时加载)
private Singleton() {}
private static Singleton instance=null;
public static Singleton getInstance() {
if(instance==null) {
instance=new Singleton();
}
return instance;
}
如果实例已经创建完,再调用getInstance,此时不涉及修改,线程安全。
如果实例没有创建,此时可能涉及到修改,如果多个线程同时修改就存在线程安全问题。主要是因为if操作和=操作不是原子的。
那么如果解决线程安全呢?加锁就是一个好办法!!!
private static Singleton instance=null;
public static Singleton getInstance() {
//区别当前是首批调用还是后续调用决定要不要加锁
if(instance==null) {
//加锁操作
synchronized (Singleton.class) {
if(instance==null) {
instance=new Singleton();
}
}
}
return instance;
}
阻塞队列
阻塞队列是一种特殊的队列。遵循“先进先出”的原则。
在Java标准库中内置了一个BlockingQueue这样的类实现阻塞队列。put阻塞式入队列,take阻塞式出队列。
阻塞队列是一种线程安全的数据结构,并具有以下特征:
- 当队列为空,尝试出队列就会产生阻塞。一直阻塞到队列里元素不为空
- 当队列已满,尝试入队列也会产生阻塞。一直阻塞到队列里元素不满为止。
生产者消费者模型
生产者是一组线程,消费者是另一组线程,交易产所是阻塞队列。
生产者消费者模型最大的用途:
- 解耦合 如A和B两个进行数据传输,此时就是A给B传数据,而如果加上C也让A给C传数据就比较麻烦,此时如果有一个中间媒介存放A的数据,谁需要就从里边取就会方便很多。
- 削峰填谷 如大坝,遇到汛期时,关闸蓄水避免下游水太多被淹,遇到旱期,开闸放水避免下游缺水出现旱灾。
借助BlockingQueue实现生产者消费者模型代码:
public static void main(String[] args) throws InterruptedException {
//创建一个阻塞队列
BlockingQueue<Integer> queue=new LinkedBlockingQueue<>();
//创建一个生产者
Thread producer=new Thread() {
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
System.out.println("生产者"+i);
try {
queue.put(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
//创建一个消费者
Thread consumer=new Thread() {
@Override
public void run() {
while (true) {
try {
int num=queue.take();
System.out.println("消费者"+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
producer.join();
consumer.join();
}
通过循环队列实现生产者消费者模型
- [head,tail)表示队列中的有效元素区间
- 入队时,把新元素放到tail位置上并且tail++。
- 出队时,把队首元素取出来,并且head++。
- 使用size记录元素个数,入队size++,出队size–。size=0队列已空,size=数组的最大长度,队列已满。
//使用循环队列实现阻塞队列
static class BlockingQueue {
private int[] items=new int[1000];
private int head=0;
private int tail=0;
private int size=0;
private Object object=new Object();
//入队列
private void put(int val) throws InterruptedException{
synchronized (object) {
while (size==items.length) {
//元素已满 进行阻塞
object.wait();
}
items[tail]=val;
tail++;
if(tail>=items.length) {
tail=0;
}
size++;
//唤醒take中的wait
object.notify();
}
}
//出队列
public int take() throws InterruptedException {
int res=0;
synchronized (object) {
while (size==0) {
//队列已空 无法出队列 进行阻塞
object.wait();
}
res=items[head];
head++;
if(head>=items.length) {
head=0;
}
size--;
//唤醒put中的wait
object.notify();
}
return res;
}
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue blockingQueue=new BlockingQueue();
//消费者
Thread consumer=new Thread() {
@Override
public void run() {
while (true) {
try {
int ret=blockingQueue.take();
System.out.println("消费者"+ret);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
//生产者
Thread producer=new Thread() {
@Override
public void run() {
for (int i = 1; i <1000 ; i++) {
System.out.println("生产者"+i);
try {
blockingQueue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
consumer.join();
producer.join();
}
定时器
相当于“闹钟”,比如浏览器访问网站时,网卡了,浏览器就会转圈圈(阻塞等待),这个等待不是无限等待,等待一定时间就显示访问超时。
Java标准库中,提供了Timer类。Timer类的核心方法是schedule,schedule有两个参数,一个是指定即将执行的任务代码,一个是指定多长时间后执行。
public static void main(String[] args) {
System.out.println("代码开始执行");
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("触发定时器");
}
},3000);//3s后执行
}
实现定时器
一个定时器里面可以安排很多任务,这些任务按照时间,谁时间先到先执行谁。
描述任务:使用Runnable描述任务
组织任务
:需要在一堆任务中找到最先要执行的任务。使用带阻塞功能的优先级队列实现。
提供schedule方法往阻塞队列中插入元素。在Timer内部有一个线程扫描队首元素。如果队首元素到时间点了就执行任务,如果没到点,就把队首元素放回队列,继续扫描。
//定时器实现
static class Task implements Comparable<Task> {
//command表示任务是什么
private Runnable command;
//time表示这个任务什么时候到时间
private long time;
public Task(Runnable command,long time) {
this.command=command;
this.time=System.currentTimeMillis()+time;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task o) {
return (int)(this.time-o.time);
}
}
static class Timer {
private PriorityBlockingQueue<Task> queue=new PriorityBlockingQueue<>();
//使用locker对象解决盲等
public Object locker=new Object();
public void schedule(Runnable command,long delay) {
Task task=new Task(command,delay);
queue.put(task);
// 每次插入新的任务都要唤醒扫描线程. 让扫描线程能够重新计算 wait 的时间, 保证新的任务也不会错过~~
synchronized (locker) {
locker.notify();
}
}
public Timer() {
//创建扫描线程,判断当前任务是不是已经到时间执行
Thread t=new Thread() {
@Override
public void run() {
while (true) {
//取队首元素判断时间是不是到了
try {
Task task=queue.take();
long curTime=System.currentTimeMillis();
if(task.time>curTime) {
//时间没有到不执行重新插入队列
queue.put(task);
//根据时间差来进行等待
synchronized (locker) {
locker.wait(task.time-curTime);
}
} else {
//时间到
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
t.start();
}
}
线程池
为了解决线程的频繁创建和销毁带来的开销,而引入线程池。当我们需要使用线程时,就从池子里取一个线程。当我们不需要这个线程的时候,就把这个线程还回池子中,这种操作就比创建销毁线程效率高很多。
在Java标准库里提供了标准的线程池ThreadPoolExecutor。其使用相对复杂,里边有很多参数。下面我们对ThreadPoolExecutor里边的参数进行说明:
- corePoolSize 核心线程数(即一个公司正式员工的数量)。
- maximumPoolSize 最大线程数(即一个公司正式员工+临时工的数量)。
- keepAluveTime 描述临时工可以摸鱼多长时间 unit为keepAluveTime的时间单位。
- workQueue 阻塞队列。
- threadFactory 线程的创建方式 通过这个参数来设定不同的线程的创建方式。
- Rejected Execution Handler 拒绝策略 当任务队列满了的时候来了新任务。丢弃最新任务或丢弃最老任务或阻塞等待或抛出异常。
Executors
由于ThreadPoolExecutor使用起来比较复杂,标准库又提供了一组其他类相当于对ThreadPoolExecutor有一层封装。
Executors 相对于一个“工厂类”提供一组工厂方法可以创建不同风格的线程池实例。
- newFixedThreadPool 创建一个固定线程数量的线程池。
- newCachedThreadPool 创建一个数量可变的线程池。
- newSingleThreadPool 创建一个只包含一个线程的线程池。
- newScheduleThreadPool 设定延时时间的线程池。
线程池的实现
static class Worker extends Thread {
private BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run() {
// 工作线程的具体的逻辑.
// 需要从阻塞队列中取任务.
while (true) {
try {
Runnable command = queue.take();
// 通过 run 来执行这个具体的任务.
command.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class ThreadPool {
// 包含一个阻塞队列, 用来组织任务.
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 这个 list 就用来存放当前的工作线程.
private List<Thread> workers = new ArrayList<>();
private static final int MAX_WORKER_COUNT = 10;
// 通过这个方法, 把任务加入到线程池中.
// submit 不光可以把任务放到阻塞队列中, 同时也可以负责创建线程.
public void submit(Runnable command) throws InterruptedException {
if (workers.size() < MAX_WORKER_COUNT) {
// 如果当前工作线程的数量不足线程数目上限, 就创建出新的线程.
// Worker 内部要能够取到队列的内容. 就需要把这个队列实例通过 Worker 的构造方法, 传过去.
Worker worker = new Worker(queue);
worker.start();
workers.add(worker);
}
queue.put(command);
}
}