多线程学习总结(三)

目录

前言:

一.wait与notify的基本认识与使用:

1.wait的两个版本:

2.wait和notify的联系和用法:

二.单例模式:

1.单例模式的含义:

2.单例模式的两种实现方法:

3.单例模式的线程安全问题:

4.懒汉模式线程安全版本的修改:

三.结语: 


前言:

从这篇笔记开始,将会进入到多线程两大模式和代码实例的深入总结,包括线程池的创建和实现、定时器的实现、阻塞队列的实现等。不过在开始这些前,先补充两个在多线程中常用的可以让线程“有序”的两个方法——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,缺一不可!

三.结语: 

除了单例模式外,在多线程中还有一个应用更加广泛的的事例模式——工厂模式,本章总结中就先不涉及到他了,下一篇笔记中将会进行总结。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值