多线程案例 --- 单例模式(饿汉懒汉)阻塞式队列
Posted 满眼*星辰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程案例 --- 单例模式(饿汉懒汉)阻塞式队列相关的知识,希望对你有一定的参考价值。
设计模式
- 单例模式
- 工厂模式(简单工厂、抽象工厂)
- 模板模式
。。。
单例模式
整个程序的运行中只存储一个对象
饿汉方式和懒汉方式
饿汉方式:
上来不管三七二十一先创建对象再说
class Singleton {
//1.创建私有的构造函数(防止其他类直接创建)
private Singleton() {
}
//2.定义私有变量(线程安全)
private static Singleton singleton = new Singleton();
//3.提供公共的获取实例的方法
public static Singleton getInstance() {
return singleton;
}
}
不用加锁(线程安全的)
缺点:
程序启动之后就会创建,但是创建完了之后有可能不会使用,从而浪费了系统资源。
懒汉方式:
当程序启动之后,并不会进行初始化,而是在什么时候调用什么时候初始化
版本一:线程不安全
非安全的单例模式——懒汉
class Singleton {
//1.创建一个私有的构造函数(防止其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static Singleton getInstance() {
if (singleton == null) {
//第一次访问
singleton = new Singleton();
}
return singleton;
}
}
这个版本在单线程下是没有问题的,但是如果有两个线程同时想要创建Singleton的实例,那么会怎么样呢?
两个线程到达if (singleton == null) {
这个条件判断的时候,连个线程的singleton都为null,所以两个线程都会执行里面的创建实例,所以就创建了两个实例,就不是单例模式了,所以并不适用于多线程,线程不安全。
证明两个创建的不是一个对象
static class Singleton {
//1.创建一个私有的构造函数(防止其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第一次访问
singleton = new Singleton();
}
return singleton;
}
}
private static Singleton s1 = null;
private static Singleton s2 = null;
public static void main(String[] args) throws InterruptedException {
//创建新线程执行任务
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getInstance();
}
});
t1.start();
//使用主线程执行任务
s2 = Singleton.getInstance();
//等待t1执行完
t1.join();
System.out.println(s1 == s2);
}
让两个线程都实例化对象,比较两个实例对象是否是==,结果输出false,不是单例模式
版本二:性能不佳
要解决线程安全的问题,最简单的方式就是加锁
给实例化方法加锁 ,synchronized
static class Singleton {
//1.创建一个私有的构造函数(防止其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static synchronized Singleton getInstance() {
if (singleton == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第一次访问
singleton = new Singleton();
}
return singleton;
}
}
private static Singleton s1 = null;
private static Singleton s2 = null;
public static void main(String[] args) throws InterruptedException {
//创建新线程执行任务
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getInstance();
}
});
t1.start();
//使用主线程执行任务
s2 = Singleton.getInstance();
//等待t1执行完
t1.join();
System.out.println(s1 == s2);
}
结果输出true,是线程安全的单例模式
但是这样实现懒汉模式又有一个问题:
不管是不是第一次访问,它都会排队执行。
我们加锁为了确保第一次访问的时候线程是安全的,但是与此同时,当我们再三访问的时候,每次都需要加锁排队,这无疑让单例模式的性能非常低,所以这种方式还有优化的空间。
版本三:性能更加
既然我们在方法外加锁太浪费性能,那我们可不可以在方法体内的加锁呢?
我们来试想一下
static class Singleton {
//1.创建一个私有的构造函数(防止其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
//第一次访问
singleton = new Singleton();
}
}
return singleton;
}
}
这种方式可以吗?不可以!!!
因为第一次获取实例的时候,两个线程都会进入singleton为null的判断里,排队完后,依然是两个线程都执行了里面实例化的对象。
所以我们可以在锁里面再加一次判断singleton是否为null
static class Singleton {
//1.创建一个私有的构造函数(防止其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//第一次访问
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式就非常完美了,因为两个线程如果是第一次实例对象,先进到第一个为空的判断中,然后排队执行锁里面的内容,谁先竞争到了锁,谁就执行里面的实例化新对象,执行完后,第二个线程进来,又做一个判断,sigleton是否实例化过了,显然,第一个线程已经实例化过了,那么第二个线程就什么也不做,跳出循环返回已经实例化过的singleton对象
这就是著名的双重效验锁
但是!!!!!这个代码还不是最完美的!!!
版本四:完美无瑕
这是因为,
singleton = new Singleton();实例化过程中,看似是一行代码,但是会存在三个步骤
- 先在内存中开辟空间(买房)
- 初始化(装修)
- 将变量 singleton 指向内存区域(入住)
这就会存在一个指令重排序的问题,所有java代码都会有的问题
指令优化(指令重排序):
执行重排序(前):1 -》 2 -》 3
指令重排序(后):1 -》 3 -》 2
这样就1- 3- 2 的顺序就是错误的,这会导致什么问题呢?
会导致线程二返回了一个空对象。
要解决这个指令重排序的问题,我们只需要用 volatile 关键字即可
class Singleton {
//1.创建一个私有的构造函数(方法其他地方直接实例化)
private Singleton() {
}
//2.创建一个私有的类对象
private static volatile Singleton singleton = null;
//3.提供统一的访问入口(方法)
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//第一次访问
singleton = new Singleton();
}
}
}
return singleton;
}
}
这个代码才是最终版的懒汉方式
自定义阻塞队列
他是基于生产者消费者模型
生产者消费者模型:生产者生产数据,消费者消费生产者产生的数据
生产者:
- 添加数据
- 当数据量满了之后,不要尝试给队列添加数据了,而是阻塞等待
- 用wait()、notify()方式进行等待和唤醒
消费者:
- 取出数据
- 当队列为空的时候,就阻塞等待
static class MyBlockingQueue {
private int[] values; //实际存储数据的数组
private int first; //队首
private int last; //队位
private int size; //队列元素的实际大小
public MyBlockingQueue(int initial) {
//初始化变量
values = new int[initial];
first = 0;
last = 0;
size = 0;
}
//添加元素(队尾)【生产者】
public void offer(int val) {
synchronized (this) {
//判断边界值
if (size == values.length) {
//队列已满
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加元素到队尾
values[last] = val;
last++;
size++;
//判断是否为最后一个元素
if (last == values.length) {
last = 0;
}
// 尝试唤醒消费者
this.notify();
}
}
//弹出元素(队首)【消费者】
public int poll() {
int result = -1;
synchronized (this) {
//判断边界值
if (size == 0) {
//队列为空,阻塞等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取元素
result = values[first];
first++;
size--;
//处理是否为最后一个元素
if (first == values.length) {
first = 0;
}
//尝试唤醒生产者
this.notify();;
}
return result;
}
}
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue(100);
//创建生产者
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//每隔 500ms 生产一条数据
while (true) {
int num = new Random().nextInt(10);
System.out.println("生产了随机数:" + num);
queue.offer(num);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
//创建消费者
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
int result = queue.poll();
System.out.println("消费了数据:" + result);
}
}
});
t2.start();
}
实现结果:
以上是关于多线程案例 --- 单例模式(饿汉懒汉)阻塞式队列的主要内容,如果未能解决你的问题,请参考以下文章