目录
单例模式
饿汉模式和懒汉模式
饿汉模式: 使用static方法new一个实例,并进行实例化操作,在类加载的同时就立即初始化实例
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
可以看到饿汉模式中getinstance方法仅仅只是读取了变量的内容,如果多个线程只是读同一个变量,不修改,此时线程仍安全。
懒汉模式: 不是立即创建实例,真正用到这个实例的时候才会真正创建这个实例,节省资源
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式中,既包含了读,又包含了修改,这两个步骤不是原子性的,存在线程安全问题。
对上面的代码进行改进,通过加锁来保障原子性,if语句解决重复加锁浪费资源的问题,给instance加上volatile来保证可见性,避免第一层if误判,如多个线程重复读可能导致编译器把都内存操作优化为读寄存器操作。
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
//最外面if语句判断是否加锁,如果没有这个if则每次调用getinstance都加锁
//大大降低了运行效率
if (instance == null) {
//类对象在一个程序中只有唯一一份,就能保证多个线程调用
//instance的时候但是针对同一个对象进行加锁
synchronized (Singleton.class) {
if (instance == null) {//判定是否要创建实例
instance = new Singleton();
}
}
}
return instance;
}
}
阻塞队列
1.线程安全
2.产生阻塞效果。
(1)如果队列为空,就会出现阻塞,阻塞到队列不为空为止。
(2)如果队列为满,尝试入队列,也会产生阻塞,直到队列不为满。
阻塞队列的作用:
1.解耦合,如生产者消费者模型中能够让服务器的请求和执行之间耦合性降低,彼此有一个交互的通道。
2.能够对于请求进行削峰填谷。即执行服务器的请求不会因为暴涨或暴跌而出现大幅度波动或者暴跌。
标准库中的阻塞队列
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new LinkedBlockingDeque<>();
queue.put("请求");
String s=queue.take();
}
自己实现一个阻塞队列 1.普通队列(数组和链表都可实现) 2.线程安全 3.阻塞队列
class MyBlockingQueue {
private int[] data = new int[1000];
private int size = 0;//有效元素个数
private int head = 0;//队首下标
private int tail = 0;//队尾下标
//Object锁对象
private Object locker = new Object();
//保证线程安全
//入队列
public void put(int value) throws InterruptedException {
synchronized (locker) {
if (size == data.length) {
//队列满了,实现阻塞等待
locker.wait();
//take成功了一个元素则可以唤醒
//wait后则一直等待,不继续执行,直到唤醒
}
//把新的元素放到tail位置
data[tail] = value;
tail++;
//接下来处理tail到达数组末尾的情况
//if(tail>=data.length){
//tail=0;
//}
//这种方法也可以处理tail达到数组末尾情况
tail = tail % data.length;
//插入完成后要修改元素个数
size++;
locker.notify();
}
}
public int take() throws InterruptedException {
synchronized (locker) {
if (size == 0) {
//如果队列为null,阻塞
locker.wait();
}
//取出head位置的元素
int ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
locker.notify();
return ret;
}
}
}
如果想要实现精准唤醒,则必须使用不同的锁对象:例如唤醒t1,则定义多把锁,唤醒t1和等待操作,则o1.notify,o1.wait;唤醒t2和等待操作,则o2.notify,o2.wait。
定时器
定时器就是达到一个设定的时间之后,就执行某个指定好的代码。
标准库中的定时器:
public static void main(String[] args) {
//定时器
Timer timer=new Timer();
timer.schedule(new TimerTask() {
//new TimerTask本质上就是runnable
//第一个参数指定即将要执行的任务代码,第二个参数指定多少时间
//之后执行(单位为毫秒)
@Override
public void run() {
System.out.println("hello");
}
},3000);
System.out.println("main");
}
自己实现一个定时器:
(1)描述任务
创建一个专门的类来表示一个定时器中的任务(Timer Task)
(2)组织任务,使用一定数据结构把一些任务放到一起
通过一定的数据结构(priorityBlockingQueue)来组织
(3)执行时间到了的任务
通过schedule方法来插入任务
(4)扫描线程不断地获取到队首元素,并且要判定执行任务的时间是否到达
//创建一个类,描述一个任务
class MyTask implements Comparable<MyTask>{
//表示任务具体做什么
private Runnable runnable;
//任务具体要啥时候干,保存任务要执行的毫米级时间戳
private long time;
//after是一个时间间隔
public MyTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis() +delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time-o.time);
}
}
class MyTimer{
//定时器内部要能够存放多个任务
//此处的队列除了需要优先级,同时还需要注意线程安全,因而用这种数据结构
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//存在一个重要的问题,就是堆的比较规则需要规定,任务这个类里不仅有runnable还有time,
//要手动指定通过time来进行比较
public void schedule(Runnable runnable,long delay){
MyTask task=new MyTask(runnable,delay);
queue.put(task);//放任务
//每次任务插入之前,都唤醒一下线程,检查队首任务是否该执行,
//这也是这里为什么用notify,而不用sleep的原因
synchronized (locker){
locker.notify();
}
}
private Object locker=new Object();
public MyTimer() {
//构造方法实现一个线程不停地去判断时间是否到了
Thread t=new Thread(()->{
//如果队列不为空,任务时间还没到,就会出现忙等
//解决方法就是指定等待时间
while (true) {
MyTask task = null;
try {
task = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
//比较一下看看当前这个任务时间到了没
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()) {
//时间没到,再塞回去
queue.put(task);
//指定一个等待时间
synchronized (locker){
try {
locker.wait(task.getTime()-curTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
//任务执行
task.run();
}
}
});
t.start();
}
}
public class test21 {
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
System.out.println("main");
}
}
线程池
进程比较重,频繁地创建销毁,开销会很大,选择进程池或者线程来解决,但线程频繁地创建销毁也有很大开销,这时候还是需要使用线程池或者协程。把线程提前创建好作为备用,那么创建和销毁线程就会比较块。
线程池相当于是用户态,相比较内核态处理的操作,效率要更高。
java标准库中的线程池
public static void main(String[] args) {
//创建一个固定线程数目的线程池
ExecutorService pool= Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
自己实现一个线程池
1.描述任务
2.组织任务(通过阻塞队列)
3.描述线程
4.组织线程
5.实现添加任务
class MyThreadpool{
//1.描述一个任务,直接使用Runnable,不需要额外创建类
//2.使用数据结构来组织
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
//3.描述一个线程,从任务队列中能够取任务执行
static class Worker extends Thread {
//当前线程池中有若干个worker线程,线程内部持有上述任务队列
private BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
Runnable runnable = null;
try {//循环地去获取任务队列任务
runnable = queue.take();
//获取到任务,开始执行
//队列如果为空,这个阻塞队列就会陷入阻塞的状态
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//4.创建一个数据结构来组织若干个线程
private List<Thread> workers = new ArrayList<>();
public MyThreadpool(int n) {
//创建若干线程,放到上述的数组中
for (int i = 0; i < n; i++) {
Worker worker=new Worker(queue);
worker.start();
workers.add(worker);
}
}
//5.创建一个方法传任务到线程池
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}