目录
前言:
从这篇笔记开始,将会进入到多线程两大模式和代码实例的深入总结,包括线程池的创建和实现、定时器的实现、阻塞队列的实现等。不过在开始这些前,先补充两个在多线程中常用的可以让线程“有序”的两个方法——wait和notify。
一.wait与notify的基本认识与使用:
1.wait的两个版本:
(1)加锁对象.wait();——此写法意为,令该线程进入无休止的阻塞等待,直到被notify或interrupt唤醒为止。
(2)加锁对象.wait(int waitTime);——此写法为,令线程进入阻塞等待,但等待时间不会超过(waitTime/1000),此处的waitTime是以毫秒级为单位的,代表最大等待时间。
2.wait和notify的联系和用法:
wait代表令它所在的线程进入阻塞等待并释放锁的效果,而notify意为令调用notify的锁对象所在的线程的wait效果被清除。
Thread c = new Thread(() -> {
for(int i = 0; i < 10; i++) {
System.out.print("c");
synchronized (object) {
object.notify();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread b = new Thread(() -> {
for(int i = 0; i < 10; i++) {
synchronized (object) {
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.print("b");
}
}
});
单纯的语言解释也许不好理解,但是通过观察代码可以看到,此处在两个线程的加锁操作中分别通过加锁对象调用了wait和notify方法而,此时,当两个线程同时调用start后,线程b就会因为读到wait方法而进入阻塞等待,并且,此处是没有超时时间的版本,这就意味着,线程b会一直阻塞等待下去,直到线程c读到了notify操作。那么,看到这里可能会有一个疑惑,线程c中的notify操作是怎么唤醒线程b中的wait的呢?这里就要注意到这个被加锁对象了,这里很明显是两个线程针对同一个对象进行加锁的操作,且,正是调用wait和notify的对象,故而,线程b可以被线程c中的notify唤醒。在这里也要注意,上述代码中,wait和notify操作是在加锁中由被加锁对象调用的,那么,可以让这两个方法在加锁操作外使用吗?那么,如果看过了代码演示之前的介绍,就应该可以轻松想到——答案是一定不可以的,因为,wait操作,它的本质是在对锁对象进行修改,wait会让其解锁,而解锁的前提是要先加上锁了。而notify操作虽然未涉及到解锁操作,但仍被Java强制要求必须放在加锁操作中由锁对象调用(在c++中则不会有这种要求,因为系统原生api没有这样的强制)。此处还必须要确保,调用wait和notify的对象为同一个对象,不然就是空打一枪,notify不会起到任何作用。
那么,只要合理使用wait和notify操作就可以达到一个线程“有序”的效果,例如上述代码,执行后打印的结果即是“cb”。
这里,十个“cb”就被打印出来了。
二.单例模式:
补充完wait和notify操作后,就正式进入到了两大模式的总结,而这里的第一个模式就是常见的单例模式。
1.单例模式的含义:
在实现单例模式前,需要先来认识一下什么是单例模式。单例模式,顾名思义,是只能有一个实例化对象的类。也就是说,这个类对象无法在类外进行new操作,那么就需要让它的构造方法是private的,这样一来,就无法在类外进行new操作了。那应该怎样获取到这个类对象呢?这里就要牵扯出单例模式的两种实现方法了。
2.单例模式的两种实现方法:
(1)饿汉模式:
饿汉模式(勤快模式,自己取的名字,个人感觉比较直观),其实例化类对象的时机就是类加载完的时候。
class lonelyMode{
//饿汉版单例模式
private static lonelyMode mode = new lonelyMode();
private lonelyMode() {}
public static lonelyMode getMode() {
return mode;
}
}
以上就是饿汉模式下的单例模式类对象实例化和创建类的代码。
(2)懒汉模式:
对比饿汉模式,懒汉模式应该就很好理解了,顾名思义,他不会像饿汉模式那样,类加载就实例化类对象。相反,懒汉模式的类对象的实例化是在需要它的时候才实例化的,但,这种实例化操作又不能在类外进行,那要怎么做呢?其实也很好办,只需要先让该引用指向空,然后在类中添加一个让类外成员使用的方法,这个方法即代表了要使用这个单例模式,同时也是完成了类对象的实例化。代码示例如下:
class lonelyMode{
//懒汉版单例模式
private static lonelyMode mode = null;
private lonelyMode() {}
public static lonelyMode start() {
if(mode == null) {
mode = new lonelyMode();
}
return mode;
}
}
通过添加if判断,判断mode是否为空这个操作,就可以避免多次的new操作,从而使类对象只被实例化一次。
3.单例模式的线程安全问题:
既然是在多线程这里提到的单例模式,那就意味着它的出现就是为了在多线程中更好的使用,既然要使用这样的一个模式,那就不得不考虑一个在多线程中至关重要的问题——线程安全问题。
饿汉模式自然不用多说,观察它的整段代码,并未涉及到对变量的修改操作,通过前面的总结可以知道,既然未涉及到修改操作,那自然也就不会有线程安全问题。反之,来看懒汉模式,在它的start方法中涉及到了修改操作,既然涉及到了修改操作就有可能是线程不安全的,就要考虑这个修改是不是“原子”的。这里是赋值操作,很显然,赋值操作那一定是“原子”的,但是那就可以直接下结论说它也是线程安全的吗?当然不可以,因为,此处很明显是先判断再赋值的一个操作,if和赋值操作应该当成一个整体考虑,那么这个时候,这个修改操作就不再是“原子”的了,自然也就是线程不安全的了。那应该如何修改在可以变成线程安全的呢?接下来的总结中就会提到。
4.懒汉模式线程安全版本的修改:
前面总结中提到,懒汉模式是一个线程不安全的版本,但由于它实例化的时机更适合实际应用,那就不得不进行线程安全问题的修改,而这个修改则分为三个步骤,下面进行说明:
既然懒汉模式的线程安全问题是由修改操作的非“原子”引起的,那么,我们只需要让这个操作变成一个“整体化”的“原子”操作即可,这里就需要用到之前在线程安全问题中提到的加锁操作了,演示如下:
class lonelyMode{
private static Object object = new Object();
//懒汉版单例模式
private static lonelyMode mode = null;
private lonelyMode() {}
public static lonelyMode start() {
synchronized (object) {
if(mode == null) {
mode = new lonelyMode();
}
}
return mode;
}
}
通过对if语段加锁,使if判断和赋值操作成为一个“整体”的“原子”操作。可能改完之后浅浅一想,似乎没有别的问题了,但是,问,依照当前代码在多线程中调用这个start方法,带来的效果是什么?答案是,第一次调用会实例化一次类对象,此后调用都不会再实例化对象了,但是!加锁操作仍会执行,只不过,加上锁之后就直接解锁了,那既然如此,这个频繁加锁的操作其实也是不必要的,因为,频繁的加锁,会让程序的执行效率大大降低(此处锁中没有额外操作,但假如锁中有一系列很复杂的操作呢?),既然如此,为了优化代码的执行效率,避免频繁的加锁操作,但是又不能让对象被多次实例化,那就需要找到加锁的条件并通过条件判断来控制加锁操作的执行。
通过观察和思考程序的执行逻辑可以看出,但对象已经不为空的时候,就没有必要再去加锁和内部的判断了,所以,加锁操作的前提条件就是对象得是null的,所以第二步的优化代码如下:
class lonelyMode{
private static Object object = new Object();
//懒汉版单例模式
private static lonelyMode mode = null;
private lonelyMode() {}
public static lonelyMode start() {
if(mode == null) {
synchronized (object) {
if(mode == null) {
mode = new lonelyMode();
}
}
}
return mode;
}
}
那么,以上就是第二步优化后的代码了,看到这里,可能会有一个疑惑,就是,这个加锁操作的判断条件既然和赋值操作的一样,那干嘛还要写两个呢?这样不会很啰嗦吗?在单线程模式下来看的确如此,但是在多线程中,这样正合适,试想一下,假如,现在线程a和线程b都执行到了加锁判断这里,然后,此时的mode也确实为空,条件成立,然后就会使两个线程中的其中一个加上锁,如果这时候没有赋值前的if判断,mode就会被直接赋值,不过mode本就为空,这样看来似乎也没问题,不过接下来,当第一个拿到锁的线程释放锁之后,第二个线程就会在加锁操作这一步开始(因为最开始就是在这里发生阻塞等待的),接着拿到锁然后往下执行,但是由于赋值操作前没有判断了,所以mode将会再一次被赋值,就会实例化多次,不再是单例模式了,所以,这两个if都至关重要,缺一不可,而这里两个判断代表的含义也不相同,只是判断条件式碰巧一样而已。
经过两步修改之后就彻底没有问题了吗?当然还是并非如此,之前总结的线程安全问题中,还有两个我们还没有考虑到,那就是内存可见性问题和指令重排序问题,此处有没有可能会触发编译器的读操作优化呢?谁都不清楚,万一触发了呢?所以,保险起见,就需要给mode加上一个volatile关键字,就可以避免这两个优化带来的问题了(如下):
class lonelyMode{
private static Object object = new Object();
//懒汉版单例模式
private volatile static lonelyMode mode = null;
private lonelyMode() {}
public static lonelyMode start() {
if(mode == null) {
synchronized (object) {
if(mode == null) {
mode = new lonelyMode();
}
}
}
return mode;
}
}
经过以上三步的操作,懒汉版单例模式的线程安全版本也就修改完了,主要注意双重if,缺一不可!
三.结语:
除了单例模式外,在多线程中还有一个应用更加广泛的的事例模式——工厂模式,本章总结中就先不涉及到他了,下一篇笔记中将会进行总结。