单例模式:是设计模式之一。代码当中的某个类,只能有一个实例,不能有多个。JDBC 的 DataSource 这样的对象就应该是单例的。
设计模式:就是“棋谱”,就是固定的一些代码套路。写代码的时候,有很多经典场景,经典场景中,也有一些经典的应对手段。
单例模式有两种:
饿汉模式就是表示很着急,就像吃完饭剩下很多碗,然后一次性把碗全洗了。就是比较着急的去创建实例。使用 static 来创建实例,并且立即进行实例化。这个 instance 对于的实例,就是该类唯一的实例。代码如下:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
public class Test {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
为了放在程序员在其他地方不小心 new 这个 Singleton 就可以把构造方法设为 private。
static 修饰的成员更准确的说,应该叫作“类成员” => “类属性/类方法”。不加 static 的成员,就是“实例成员” => “实例属性/实例方法”。
在 Java 程序中,一个类对象只存在一份(JVM 来保证),进一步也就保证了类的 static 成员只有一份。
类对象和对象不是一个东西:
类:相当于实例的模板,基于模板可以创建出很多对象。
类对象:类名字.class 文件,被 JVM 加载到内存之后,表现出的模样。
类对象里面就有 .class 文件中的一切信息。包括:类名,属性。
懒汉模式主要就是,不是立即初始化实例。因为不是立即初始化,所以只有在调用的时候,才会创建实例。
如何保证懒汉模式的线程安全? 加锁,通过创建实例的代码加锁就可以了,加锁的时候,可以直接指定类对象 .class 作为锁对象。加锁之后,线程安全问题就得到了解决,但是又有了新的问题。多线程调用获取信息的时候,就可能同时涉及到读和修改。但是一旦被初始化之后,就只剩读操作了。代码如下:
class Singleton2 {
private static volatile Singleton2 instance = null;
private Singleton2() {}
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Test2 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
因为如果多个线程,都去读 getInstance 那么就可能导致优化为直接去寄存器读。所以我们加上 volatile 来避免编译器优化。
和饿汉模式的区别就是,懒汉模式只有在使用的时候,才会创建实例,饿汉模式在类加载的时候就会创建实例。
先进先出,相对于普通队列,又有其他方面的功能:
通过上面这种特性,就可以实现 “生产者模型” 。就像我们烤串,有人烤,有人吃,然后烤好的放在烤盘上面。对于吃烤串来说,烤盘就是交易场所。此处的阻塞队列就可以作为生产者消费者模型当中的交易场所。
生产者消费者模型,是实际开发当中非常有用的一种多线程开发手段,尤其是在服务器开发场景当中。假设有两个服务器 A 和 B,A 作为入口服务器直接接受用户的网络请求,B 作为应用服务器,来给 A 提供一些数据。如图:
如果不使用生产者消费者模型,此时 A 和 B 的耦合性是比较强的。在开发 A 代码的时候,就得充分了解到 B 提供的一些接口,开发 B 代码的时候,也得充分了解到 A 是怎么调用的。一旦想把 B 换成 C ,A 的代码就需要较大的改动。而且如果 B 挂了,也可能直接导致 A 也顺带挂了。
未使用生产者消费者模型的时候,如果请求量突然暴涨。A 暴涨=> B 暴涨,A 作为入口服务器,计算量较小,不会产生问题。B 作为应用服务器,计算量可能很大,需要的系统资源也更多,如果请求更大了,就可能导致程序挂了。如图:
如果使用阻塞队列的话,A 的请求暴涨 => 阻塞队列的请求暴涨,由于阻塞队列没啥计算量,只是存数据,所以抗压能力就更强。B 这边依然按照原来的速度进行处理数据,就不会受到 A 的暴涨。所以就不会引起崩溃。也就是 “削峰”。这种峰值很多时候不是持续的,过去之后就恢复了。B 仍然是按照原有的频率来处理之前积压的数据,就是 “填谷” 。
实际开发当中:阻塞队列不是一个简单的数据结构了,而是一个/一组专门的服务器程序,提供的功能不仅仅是队列阻塞。还会在这些基础上面提供更多的功能(数据持久化存储,多个数据通道,多节点备份,支持控制面板,方便配置参数),又叫”消息队列“。
通过 BlockingQueue 来实现阻塞队列,代码如下:
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingDeque<>();
//入队
queue.put("hello");
//出队
String s = queue.take();
}
在 new 对象的时候,这里选择用链表实现的阻塞队列。阻塞队列也有 offer poll peek 但是这些没有阻塞功能。
用数组就是通过循环队列来实现。如下图:
出队列就是把 head 位置的元素返回去,并且 head++。当 tail 加满的时候,就回到队列头。所以重要的就是区别空队列和满队列。所以我们创建一个变量来记录元素的个数:size == 0 就是空,size == arr.length 就是满。
保证线程安全:
实现阻塞效果:通过使用 wait 和 notify 机制来实现阻塞效果。
阻塞条件:
代码如下:
class MyBlockQueue {
private int[] data = new int[1000];
private int size = 0;
private int head = 0;
private int tail = 0;
private Object locker = new Object();
//入队列
public void put(int value) throws InterruptedException {
synchronized (locker) {
if (size == data.length) {
//队列满了。针对哪个对象加锁,就使用哪个对象 wait
//put 当中的 wait 要由 take 来唤醒,只要 take 成功一个元素,就可以唤醒了
locker.wait();
}
//队列不满,把新的元素放入 tail 位置上
data[tail] = value;
tail++;
//处理 tail 到达数组末尾的情况
if (tail >= data.length) {
tail = 0;
}
size++;
locker.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
synchronized (locker) {
if (size == 0) {
//说明队列为空,就需要等待,就需要 put 来唤醒
locker.wait();
}
int ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
//就说明 take 成功了。然后唤醒 put 中的等待。
locker.notify();
return ret;
}
}
}
public class MyBlockingQueue {
private static MyBlockQueue queue = new MyBlockQueue();
public static void main(String[] args) {
//如果有多个生产者和多个消费者,就再多创建几个线程
Thread producer = new Thread(() -> {
int num = 0;
while (true) {
try {
System.out.println("生产了:" + num);
queue.put(num);
num++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(() -> {
while (true) {
int num = 0;
try {
num = queue.take();
System.out.println("消费了:" + num);
//消费慢了,但是可以一直生产。1000 之后,
// 队列满了,所以就阻塞了。直到消费了一个。
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
运行结果如下:
put 和 take 的相互唤醒之间的关系如下:
像是一个闹钟,在一定时间之后,被唤醒并执行某个之前设定好的任务。就像是长时间网页加载不出来,就显示连接不到网页。
通过 Timer 的 schedule 任务来设计任务计划,Timer 内部是有专门的线程,来负责执行注册的任务,所以执行完之后,并不会马上退出线程。
管理任务:
代码如下:
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello Timer");
}
}, 3000);//就是在 3 秒之后执行这个任务,
System.out.println("main");
}
运行结果如下:
线程并没有结束,因为 Timer 内部有专门的线程,来负责执行注册的任务的。
代码如下:
class MyTask implements Comparable<MyTask> {
//任务具体要干什么
private Runnable runnable;
//任务具体啥时候干,保存任务要执行的毫秒级时间戳
private long time;
public long getTime() {
return time;
}
//after 是一个时间间隔,不是绝对的时间戳的值
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer {
//定时器内可以存放很多任务,要考虑到多线程问题,还要注意到线程安全。
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public void schedule(Runnable runnable, long delay) {
MyTask task = new MyTask(runnable,delay);
queue.put(task);
//每次任务插入成功之后,都唤醒一下扫描线程,重新检查一下队首的任务时间是否到了
synchronized (locker) {
locker.notify();
}
}
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()) {
//说明时间没到
queue.put(task);
//指定一个等待时间,时间到了之后,等待自然也就唤醒了。
// sleep 不能被中途唤醒, wait 是可以被中途唤醒的。
synchronized (locker) {
locker.wait(task.getTime() - curTime);
}
} else {
//时间到了
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
写代码的时候,要给循环加限制。如果队列中的任务是空着的,就还好,这个线程就阻塞了。就怕队列不为空,并且任务时间还没到,就会一直看任务,浪费资源,也就是忙等。忙等是很浪费 CPU 的。避免忙等:通过设计查询比率,可以通过 wait 这样的机制来实现。wait 有一个版本,指定等待时间(不需要 notify,时间到了自然唤醒)就不会忙等了。main 代码如下:
public class Test5 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello Timer");
}
},3000);
System.out.println("main");
}
}
因为进程比较重,频繁的创建和销毁,开销就会大,解决方法:进程池 or 线程:
ThreadPoolExecutor 是标准库的线程池,不过使用起来有点麻烦。构造方法很多:
重点看第四个构造方法,参数最全,涵盖了之前所有的:
最重要的还是这两个参数,就是需要指定多少个线程:
常见问题:有一个程序,这个程序要并发的/多线程的来完成一些任务,如果使用线程池的话,这里的线程数设为多少合适?
这个问题的答案是不确定的。因为指定线程池的个数的时候,不能直接确定线程数,要通过性能测试的方法找到合适的值。
标准库当中,还有简化版本的线程池:Executors
代码如下:
public static void main(String[] args) {
//创建固定线程数目的线程池,参数指定了线程个数
ExecutorService pool = Executors.newFixedThreadPool(10);
//创建一个自动扩容的线程池,会根据任务量来自动进行扩容
Executors.newCachedThreadPool();
//创建只有一个线程的线程池
Executors.newSingleThreadExecutor();
//创建一个带有定时器功能的线程池
Executors.newScheduledThreadPool(2000);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
线程池里面要有:
代码如下:
class MyThreadPool {
//1、描述一个任务,直接使用 Runnable 不需要额外创建类了。
//2、使用一个数据结构来组织若干个任务
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
//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) {
try {
//循环的去获取队列中的任务,如果队列为空,就直接阻塞。如果不是空,就去获取里面的内容
Runnable 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) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyThreadP {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
});
}
}
}