【JavaEE】多线程案例
1.单例模式
单例模式就是在设计某个程序时,某个类只能实例一次对象,如果再次实例一个对象就会报错。然而我们人类有时也是会出错的,只有机器是固定的,我们就是通过编译器编写一个强制要求的代码实现单例模式。
单例模式又分两种,一种是饿汉模式,另一种是懒汉模式。
1.1饿汉模式
饿汉模式是通过方法得到已创建的实例对象,而实例对象是在类加载时创建的(比较急切的想要)。
class Singleton{private static Singleton singleton = new Singleton(); //将其实例对象设置为私有,并设置为静态变量,以方便别的成员调用public static Singleton getSingleton(){//通过方法获得已创建的对象return singleton;}private Singleton(){//将调用方法设置为私有,为了防止重新实例对象(重新实例会报错),}
}
1.2懒汉模式
懒汉模式是在第一次使用时创建的(比较从容,用到了再创建)。
针对这两个模式分析,如果都再多线程条件下是否线程安全?
饿汉模式安全,懒汉不安全。之前讲述的线程安全问题出现的情况有:多个线程同时修改一个变量等。何况懒汉模式中还涉及到读取和修改,更容易被多线程穿插【在判断是否为空时,先读取后存在并执行另一个线程,这样就是穿插,如下图】。解决线程安全问题的主要做法就是:加锁。
加锁就是要阻止存在穿插行为,既然如此,那我们直接在判断是否为空前加锁,如下图:
我们在深入研究,又出现了新的问题。我们实现懒汉模式只需要创建一次对象,而我们为了防止第一次创建而加锁导致后面的每一次调用对象都需要加锁,然而加锁就会出现锁冲突,锁冲突就容易导致阻塞等待。解决这个问题只需要添加 if 从句,判断是否是第一次创建。
指令重排序:编译器为了提高效率,可能会调整代码顺序(前提是保证逻辑不变)。
那这样就没有问题了吗?答:在创建对象的过程中可能出现指令重排序。new操作可以拆分成三步,第一步:申请空间;第二步:在内存空间上构造对象(构造方法);第三步:把内存的地址赋值给new的对象singlelazy。指令重排序可能将顺序调整为1 3 2,这样在单线程也是可以执行的。就是如果是多线程下,可能就会出现问题,比如:线程一执行到new操作,指令重排序将顺序调整后,先执行第一步(必须先有空间才能之后后面操作),再执行第三步将内存地址赋值给singlelazy ,此时的内存中是一个还没有初始化的非法对象。然后线程二开始执行,此时线程二中的singlelazy是非空的并且内存地址是没初始化的,然后 if 从句不满足直接跳出,最后将没有初始化的地址给了线程二中创建对象的变量,此时它指向的内存空间是非法的就出现问题了。
针对这个问题,我们可以用到volatile【之前提到的内存可见性也是编译器为了优化】。以下是懒汉模式单例模式的代码(此代码如果仔细研究还是存在些问题,正常下使用也是可以的):
class Singletonlazy{private static volatile Singletonlazy singletonlazy = null;//先将变量置为空public static Singletonlazy getSingletonlayz(){if (singletonlazy == null) {synchronized (Singletonlazy.class) {if (singletonlazy == null) {return singletonlazy = new Singletonlazy();}}}return singletonlazy;}private Singletonlazy(){}
}
2.阻塞队列
阻塞队列:
1.线程安全
2.带有阻塞特性:
(a) 如果队列为空,继续出队列就会发生阻塞,阻塞到其他线程往队列里添加元素为止;
(b) 如果队列为满,继续入队列就会发生阻塞,阻塞到其他线程从队列中取走元素为止;
阻塞队列最大的用处就是用来实现“生产者消费者模型”;
生产者消费者模型
生产者负责生产物品,而消费者负责消耗东西。当生产者生产过快时就可以休息会;相反当生产者生产过慢时消费者就需要等待生产者生产物品。
生产者消费者模型意义
1.解耦合
两个模块,联系越大耦合越高。对分布式系统来说,解耦合的意义是很大的。
2.削峰填谷
短时间内收到大量请求,服务器会把大量的请求写入阻塞队列中,其他服务器则正常从阻塞队列中获取请求。
实现方式
在Java标准库里已经提供了阻塞队列,我们直接使用即可,但是身为未来的程序员,我们还是需要知道它的底层实现原理并能熟习。
我们先讲标准库里的:BlockingQueue既能基于数组又能基于链表的,它是继承自Queue,所以可以使用Queue中的各种方法,但是这些都不具备 “阻塞” 的特性。put:阻塞式入队列;take:阻塞式出队列;
现在是我们自己实现的阻塞队列
其实很简单,我们只需要在普通的队列中加上线程安全以及阻塞即可,一个普通的队列怎么实现基于数组又基于链表,那就需要拿出我们的环形队列。
环形队列
想要实现阻塞功能,可以通过wait 、notify 实现,put 被阻塞等待 take 来唤醒,而 take 被阻塞等待 put 来唤醒。得到以下代码:
这个代码还有一个问题,就是当 put 方法因为队列满了,进入 wait 之后,wait 被唤醒时队列不一定时不满的。interrupt 方法是可以中断 wait 方法的,使用 interrupt 唤醒时会出现 InterrupttedException ,这个异常就是 wait 抛出的,我们使用 throws 是没有问题的,但是如果使用 try/catch 是有问题的,如果出现异常就直接往下进行了,接着就会把 tail 指向的元素覆盖掉,就相当于把一个有效的元素删掉了,并且此时队列还是满的,size 还会继续增加,后续还有继续put 。所以在使用 wait 时一定要注意是 notify 唤醒的还是 interrupt 唤醒的。
针对这个问题如何解决呢?可以在 wait 结束后再判断是否 size 超过最大限制。
执行完下一个 if 后又面临同样的问题【interrupt唤醒还是notify唤醒】,那就接着 if ,如此一来就成了死循环了,不如直接用 while 实现知道队列不满。
在这过程中,size 、head 、tail 这三个变量不断的读取、修改,为了防止内存可见性,我们都给它添加一个 volatile 。以下就是阻塞队列实现的全部代码:
class MyBlockingQueue{private String[] queue = new String[20];private volatile int size;//这里选择通过一个变量来记录元素个数private volatile int head;//头部private volatile int tail;//尾部private final Object lock = new Object();public void put(String elem) throws InterruptedException {synchronized (lock) {while (size == queue.length) {lock.wait();}queue[tail] = elem;size++;tail++;if (tail == queue.length) {tail = 0;}lock.notify();}}public String take() throws InterruptedException {synchronized (lock) {while (size == 0) {lock.wait();}String temp = queue[head];size--;head++;if (head == queue.length) {head = 0;}lock.notify();return temp;}}
}
接着我们针对这个阻塞队列实现一个生产者消费者模型:
这里我们只实现 生产者较慢的情况,剩下的生产者快就是将消费者速度降低(直接在消费者中添加sheep可以实现),代码及结果如下:
生产者生产一个就消费者消费一个,下面是生产多个的结果