案例一:单例模式
说明:**如其名,单例模式就是一个程序只允许某个类型创建一个对象(只允许有一个实例)。**可以想象,如果一个类背后实现了非常多的数据,当程序员创建了这个类的对象时,需要消耗很多时间,占用很多资源才能把这个对象创建好,如果有人失误在代码中多new了这种对象,不敢想象要浪费多少资源。
单例模式是一种设计模式。单例模式的具体实现方式有很多,最常见的就是"懒汉"和"饿汉"两种。
1.饿汉模式
java
public class HungryMode {
private static HungryMode instance = new HungryMode();
public static HungryMode getInstance(){
return instance;
}
//重要步骤,使用private修饰构造方法,禁止创建实例
private HungryMode(){
}
}
class Main{
public static void main(String[] args) {
HungryMode instance = HungryMode.getInstance();
//报错
// HungryMode instance2 = new HungryMode();
}
}
说明:正所谓饿汉,他一定很饥饿,看见食物就立马吃掉,就像上述代码在创建成员变量instance时,直接给成员变量赋值。
2.懒汉模式
由于懒汉的实现方式,在不同背景下代码书写方式不同;
2.1单线程背景下的懒汉模式
java
public class LazyMode {
private static LazyMode instance = null;
public static LazyMode getInstance(){
return instance = new LazyMode();
}
//重要一步,没有这一步就不是单例模式了
private LazyMode(){
}
}
class Main2{
public static void main(String[] args) {
LazyMode lazyMode = LazyMode.getInstance();
//报错
// LazyMode lazyMode1 = new LazyMode();
}
}
说明:之所以叫懒汉,就是当正真要获取对象时才实例对象,就像上述getInstance();打个比方,生活中两口子吃完饭,把全部的4个碗丢到洗碗槽,等到下次需要多少个盘子就洗几个盘子出来用,这样效率其实就高很多,因为后面我可能只会用到2个盘子,3个盘子,永远不用到4个盘子,如果一吃完就洗完盘子就多余洗那几个,浪费时间,浪费资源。
注意:在计算机中懒是褒义词。
2.2多线程背景下的懒汉模式
上述单线程下的懒汉模式代码,在多线程背景下使用会出现实例多个对象的现象。
java
public class LazyMode {
private static LazyMode instance = null;
public static LazyMode getInstance(){
return instance = new LazyMode();
}
//重要一步,没有这一步就不是单例模式了
private LazyMode(){
}
}
class Main2{
public static void main(String[] args) {
Thread t1 = new Thread(()->{
LazyMode lazyMode = LazyMode.getInstance();
//打印哈希值
System.out.println(lazyMode);
});
Thread t2 = new Thread(()->{
LazyMode lazyMode = LazyMode.getInstance();
System.out.println(lazyMode);
});
t1.start();
t2.start();
}
}
每个线程getInstance()方法都会实例一个新对象,这就不是单例模式了,因此要设计一个在多线程背景下能正常运行的懒汉模式。
java
public class MultithreadingLazyMode {
private static MultithreadingLazyMode instance = null;
public static Object lock = new Object();
public static MultithreadingLazyMode getInstance(){
synchronized(lock){
//如果instance还未被赋值
if(instance==null){
return instance = new MultithreadingLazyMode();
}
return instance;
}
}
}
class Main3{
public static void main(String[] args) {
Thread t1 = new Thread(()->{
MultithreadingLazyMode instance = MultithreadingLazyMode.getInstance();
System.out.println(instance);
});
Thread t2 = new Thread(()->{
MultithreadingLazyMode instance = MultithreadingLazyMode.getInstance();
System.out.println(instance);
});
t1.start();
t2.start();
}
}
大家觉得这个代码有问题吗?有的话,问题出在哪?
问题:这样的代码使用起来效率太低了,每次在getInstance()获取对象时都会阻塞一下,所以需要改进。
修改后:
java
public class MultithreadingLazyMode {
private static MultithreadingLazyMode instance = null;
public static Object lock = new Object();
public static MultithreadingLazyMode getInstance(){
//第一个if是判断是否需要加锁,不需要则返回对象
if(instance==null){
synchronized(lock){
//第二个if是判断是否需要给引用new对象。这个if也是很关键的,如果没有第二层if,其他线程进入外层if代码块,
// 刚好另个线程给引用创建好实例了,后面其他线程又会给引用进行实例对象
if(instance==null){
return instance = new MultithreadingLazyMode();
}
}
}
return instance;
}
}
上述两个if的作用是不同的,相信大家仔细想想还是很轻松就能理解的。
我们再看看修改后的代码,效率的问题解决了,那安全问题是否存在呢("线程安全问题")。
仔细一看天塌了。instance = new MultithreadingLazyMode() 语句存在指令重排序问题,
new MultithreadingLazyMode() 在操作系统中正常执行顺序分为三步操作:
- 申请内存
- 在空间上构造对象
- 内存空间的首地址,赋值给引用
正常来说执行顺序是1,2,3,但也有可能出现1,3,2的情况。在1,3,2的基础上,可能发生图中的情况。

(注意:引用是被static修饰的成员变量,也就是类变量了)
为了避免发生上面的"指令重排序问题",使用volatile修饰引用
最终,多线程下的懒汉模式代码:
java
public class MultithreadingLazyMode {
private static volatile MultithreadingLazyMode instance = null;
public static Object lock = new Object();
public static MultithreadingLazyMode getInstance(){
//第一个if是判断是否需要加锁,不需要则返回对象
if(instance==null){
synchronized(lock){
//第二个if是判断是否需要给引用new对象。这个if也是很关键的,如果没有第二层if,其他线程进入外层if代码块,
// 刚好另个线程给引用创建好实例了,后面其他线程又会给引用进行实例对象
if(instance==null){
return instance = new MultithreadingLazyMode();
}
}
}
return instance;
}
}
案例二:阻塞队列
1.阻塞队列说明
说明:阻塞队列是一种特殊队列,也满足"先进先出"原则。
阻塞队列是一种线程安全的数据结构,它具有以下特点
- 当队列满时,再向队列添加元素就会产生阻塞等待,直到其他线程从队列拿走元素。
- 当队列为空,再向队列取元素会产生阻塞等待,直到其他线程向队列中添加元素。
阻塞队列的一个典型应用场景就是**"生产者消费者模型"。**
生产者消费者模型 很好理解。比如家中三个人包饺子,我擀皮,另两个人包,我生产皮给另两个人,我就是生产者,他们就是消费者。++我擀的比较快,他们包的慢,就会产生阻塞等待,这时候我就等他们把堆起来的皮先使用掉,我才能继续擀,我擀的慢,则反之。++(下划线部分就是阻塞队列作用)
生产者消费者模型应用在企业中,如客户端与服务器的生产消费关系。
客户端(浏览器等)发送请求给服务器,服务器处理完请求,然后返回响应给客户端,你以为发送的请求立马就被处理了,其实并不是。发送的请求被一个专门接收请求的服务器接收后,再把请求交给另一个正真对请求做处理的服务器,如图:

注意:服务器处理请求会消耗硬件资源,包括不限于CPU、内存、硬盘、网络带宽资源。
注意:阻塞队列太重要了,甚至会把队列做成单独的服务器。消息队列中可以有N个队列。
如图的服务器搭配会有严重安全隐患。上游服务器A激增海量请求时,入口服务器A干的活简单,处理单个请求消耗的资源少可能不会挂,但是下游服务器B处理请求的逻辑复杂,就算服务器B配置再高,在面临大量请求时,资源消耗殆尽你也得挂,所以一般好的企业会在服务器A、服务器B间放入"消息队列",这样就解决大量请求把下游服务器搞挂的问题。如图:

当服务器A传的请求过多,消息队列会阻塞,等到队列中的元素被服务器B拿走处理掉一些服务器A才能往消息队列中添加元素(请求);相反 消息队列中没有元素也会阻塞服务B,等到服务器A往里添加元素(请求)服务器B才能获取请求并处理。
2.阻塞队列语法
BlockingQueue(阻塞队列)是一个接口,实现它的类有
ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。
BlockingQueue 有put、take、offer、poll和peek等方法,但带有阻塞效果的只有put和take方法。
- put方法用于阻塞式入队列,如果队列空间满了则阻塞;
- take方法用于阻塞式出队列,如队列为空则阻塞。
3.阻塞队列实战
java
package BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class TestBlockingQueue {
public static void main(String[] args) {
//ArrayBlockingQueue的put和take具有阻塞等待特点,重写后的offer和poll也有阻塞特点
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);//capacity(容量):10个元素
//生产者消费者模型
//生产者
Thread t1 = new Thread(()->{
//生产
for (int i = 0; i < 1000; i++) {
try {
queue.put(""+i);
System.out.println("生产任务:"+i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//消费者
Thread t2 = new Thread(()->{
while(true){
try {
//消费
System.out.println("消费任务"+queue.take());
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}

案例三:定时器
1.定时器说明
说明:定时器类似于闹钟,到某个时间就自动执行一些代码操作,如网络通信中,对方500ms内没返回数据,则断开尝试重连。
还比如,一个数据3秒后删除。
2.定时器语法
Timer (定时器)类,它的核心方法是schedule;
schedule 方法的参数类型为TimerTask和long。TimerTask是一个抽象类,并实现了Runnable接口,如图:
上述是使用较多的两个schedule方法,第一个schedule意为在现在多少毫秒后执行任务,第二个schedule意为某个时间点执行任务。
3.定时器实战
java
package Timer;
import java.util.Timer;
import java.util.TimerTask;
public class TestTimer {
public static void main(String[] args) {
//定时器的使用
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("完成任务");
}
},3000);
}
}
上述代码:schedule方法第一个参数实参是匿名内部类,第二个实参为3000。运行程序3秒后打印"完成任务"。
除了这种"单线程定时器"还有线程池实现了定时器:(不了解线程池的先看下面的线程池讲解)
java
package Timer;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestTimer2 {
public static void main(String[] args) {
//有一种线程池实现了定时器
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(4);//线程数目
scheduledExecutorService.schedule(()->{
System.out.println("定时线程池");
},1000, TimeUnit.MILLISECONDS);
//关闭所有线程
scheduledExecutorService.shutdown();
}
}
上述代码schedule的第三个实参是时间单位,TimeUnit是一个枚举类,可以枚举毫秒,纳秒等时间单位。
案例四:线程池
1.线程池说明
说明:线程池中有不少线程(可指定数目)需要时直接拿来用就行了(提高开发效率)。在以前,每有一个客户端连接服务器就得创建一个线程,用完后又销毁线程,这样不断创建销毁造成的开销太大,++于是人们干脆先创建一批线程,让这些线程不断执行发来的任务,因此线程池就诞生了。++
线程池小总结:线程池最大的优点就是节约了频发创造线程、销毁线程的资源消耗。
2.线程池语法
Executors(线程池)类
标准库中的线程池
- Executors.newFixedThreadPool(10)能创建出包含10个固定线程数目的线程池。
- Executors.newCachedThreadPool()创建会根据需要增加线程数目的线程池。
- 这两个方法的放回类型为ExecutorService
- 线程池通过ExecutorService es=Executors.newFixedThreadPool(10),es.submit提交任务到线程池。
Executors除了上面两种创建线程池的方法还有许多,创建只有一个线程的线程池(newSingleThreadExecutor),适合执行有顺序要求的任务(串行执行)。
还可以创建能延迟执行任务的线程池newScheduledThreadPool,这个方法放回的类和案例三最后,"线程池实现定时器"的类是一样的,都是ScheduledExecutorService 。
3.线程池实战
java
package package1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo38 {
public static void main(String[] args) {
//可固定线程数量的线程池
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
//根据需要自动增加线程数的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
int id=i;
threadPool.submit(()->{
System.out.println("hello "+id+Thread.currentThread().getName());
});
}
//shutdown能关闭所有线程池中的线程,但并不能保证线程池内的任务一定能执行完
//awaitTermination方法,能等到线程池中的任务执行完再关闭所有。
threadPool.shutdown();
}
}