多线程案例 -- 单例模式阻塞队列
Posted Putarmor
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程案例 -- 单例模式阻塞队列相关的知识,希望对你有一定的参考价值。
设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。简单说:
模式:在某些场景下,针对某类问题的某种通用的解决方案。
场景:项目所在的环境
问题:约束条件,项目目标等
解决方案:通用、可复用的设计,解决约束达到目标。
设计模式分类:
1.创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。(如单例模式、工厂模式【简单工厂、抽象工厂】)
2.结构型模式:把类或对象结合在一起形成一个更大的结构。(如:适配器模式、桥联模式)
3.行为型模式:类和对象如何交互,及划分责任和算法。(如:访问者模式、模板模式)
接下来,我们主要看一下单例模式:
一.单例模式
单例模式:整个程序的运行中只存储一个对象或者说某个类只能有一个实例,提供一个全局的访问点。对于单例模式而言,它的创建方式有两种:饿汉方式
与懒汉方式
。
饿汉方式:
饿汉方式比较粗暴,什么都不管,先创建一个对象再说:
优点:线程安全(得益于定义的static私有变量,随程序启动而加载)
class Single{
//1.声明‘私有’的构造函数,为了防止其他类进行创建
private Single(){
}
//2.定义私有变量
private static Single single = new Single();
//3.提供公共的获取实例的方法
public static Single getInstance(){
return single;
}
}
可以看出,单例模式私有构造方法不让其他类使用去创建实例对象。
饿汉方式缺点:程序启动之后就会创建对象,但是创建结束之后可能不使用,浪费了系统资源。
懒汉方式:
程序启动之后,不会进行初始化操作,而是什么时候调用什么时候初始化:
懒汉示例:
class Single{
//1.创建私有的构造函数
private Single(){
}
//2.创建一个私有的类对象
private static Single single = null;
//3.提供统一的访问入口
public static Single getInstance(){
//如果第一次访问则创建,以后直接返回
if(single == null){
single = new Single();
}
return single;
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Single single1 = Single.getInstance(); //创建第一个对象
Single single2 = Single.getInstance(); //创建第二个对象
System.out.println(single1 == single2);
}
}
从结果上看起来没有什么问题,但实际上这种方式是线程不安全的,之所以我们看到结果为true,是因为使用的是单个线程。
线程不安全原因:当多个线程访问getInstance()时,线程判断single都为null,因此会进行new操作,这样创建了不同的对象,导致不是单例模式。
1.线程不安全版本
lass Single2{
//1.创建私有的构造函数
private Single2(){
}
//2.创建一个私有的类对象
private static Single2 single = null;
//3.提供统一的访问入口
public static Single2 getInstance(){
//如果第一次访问则创建,以后直接返回
if(single == null){
try {
Thread.sleep(1000); //让线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
single = new Single2();
}
return single;
}
}
public class ThreadDemo5 {
private static Single2 s1 = null;
private static Single2 s2 = null;
public static void main(String[] args) throws InterruptedException {
//创建新线程执行任务
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Single2.getInstance();
}
});
t1.start();
//使用主线程执行任务
s2 = Single2.getInstance();
t1.join();
System.out.println(s1 == s2);
}
}
可以发现子线程与主线程都进行了实例化对象操作,创建的是两个不同的对象,因此不是单例模式!
2.加锁版本,解决线程不安全
对于上面的代码而言,其执行结果为true,表明它是线程安全的单例模式。
缺点:无论是否第一次访问getInstance(),线程都会排队执行;我们给程序加锁是为了保证第一次访问时线程安全,而每次都加锁会导致单例模式的性能很低。
3.双重效验锁(完美提升性能)
public static Single3 getInstance(){
//如果第一次访问则创建,以后直接返回
if(single == null){
synchronized (Single3.class){
if(single == null){
single = new Single3();
}
}
}
return single;
}
在synchronized锁内再加一次为空判断,当两个线程第一次访问getInstance()时,先进到一个为空判断,然后排队执行锁中的内容;这个过程中谁先竞争到锁,谁就率先执行实例化对象操作,执行结束后,第二个线发现single对象不为空已经被实例化,因此不进到if语句中,直接返回实例化后的single对象。
但是这种方案还不是最好的,有点小瑕疵。。。
4.优化方案
single = new Single()
分析上面这行代码:
实例化过程中,看起来一步,但实际上分了三步完成了实例化操作:
①先在内存中开辟空间(相当于买房)
②初始化操作(相当于装修房子)
③引用变量指向内存区域对象(相当于入住)
这里面会存在指令重排序
问题:
比如:原来执行顺序① ② ③ 指令重排序后:① ③ ②
那么指令重排序后执行程序,会出现什么结果呢?
线程1先开辟了内存空间,然后将变量指向了内存空间,此时时间片用完;线程2执行程序时发现single不为null(引用已经指向内存,只不过未初始化),直接返回了一个null对象。
那么如何解决这个问题呢?
使用volatile关键字解决内存可见性问题以及指令重排序
最终懒汉单例模式版本:
class Single3{
//1.创建私有的构造函数
private Single3(){
}
//2.创建一个私有的类对象
private static volatile Single3 single = null;
//3.提供统一的访问入口
public static Single3 getInstance(){
//如果第一次访问则创建,以后直接返回
if(single == null){
synchronized (Single3.class){
if(single == null){
single = new Single3();
}
}
}
return single;
}
}
二.阻塞队列
基于生产者消费者模型实现
生产者消费者模型:生产者生产模型,消费者消费生产者生产的数据。
生产者:添加数据;当队列已满时,不要给队列尝试添加数据,而是让线程阻塞等待;利用wait()和notify()等待和唤醒(实现线程间通讯)
消费者:取出数据,阻塞点在于:队列为空时
/**
* 自定义阻塞队列
*/
public class ThreadDemo7 {
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;
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++];
size--;
// 判断是否为最后一个元素
if (first == values.length) {
first = 0;
}
// 尝试唤醒生产者
this.notify();
}
return result;
}
}
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);
// 生产者
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 每隔 500 毫秒生产一条数据
while (true) {
int num = new Random().nextInt(10);
System.out.println("生产了随机数:" + num);
myBlockingQueue.offer(num);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
// 创建消费者
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
int result = myBlockingQueue.poll();
System.out.println("消费了数据:" + result);
}
}
});
t2.start();
}
}
基于阻塞队列,可以看出生产者生产了一个数据,消费者消费了一个数据;并不是生产者生产一个数据,消费者无限消费下去。
以上是关于多线程案例 -- 单例模式阻塞队列的主要内容,如果未能解决你的问题,请参考以下文章