多线程(JavaEE初阶系列4)

目录

前言:

1.单例模式

1.1饿汉模式

1.2懒汉模式

1.3结合线程安全下的单例模式

1.4单例模式总结

2.阻塞式队列

2.1什么是阻塞队列

2.2生产者消费者模型

2.2.1 上下游模块之间进行“解耦合”

2.2.2削峰填谷

2.3阻塞队列的实现

结束语:


前言:

在上节中小编主要与大家分享了多线程中遇到的不安全问题以及如何进行解决,那么这节中小编与大家讲解一下在多线程中的案例,在次之前我们先来了解一下什么是设计模式?设计模式就好比象棋中的“棋谱”,在下棋的时候前人总结的一些固定套路,如果我们按照套路来走局势就不会太吃亏。在软件开发中也有很多常见的“问题场景”,针对这些问题场景,大佬们总结出了一些固定的套路,按照这个套路来实现代码就不会太吃亏。在多线程中的设计模式有很多,但是尤为经典的还是单例模式和阻塞式队列这两个,所以今天小编就与大家聊聊这两个模式。

1.单例模式

单例模式能保证在某个类在程序中只存在一份实例,而不会创建出多个实例。在单例模式中具体的实现方法有两种“饿汉”和“懒汉”两种。下面就给大家分别展开聊聊这两种。

1.1饿汉模式

饿汉你可以理解为“急迫”,可以以生活中吃完饭洗碗来举个例子,如果你吃完饭就把碗洗了此时就是比较“急迫”,就是一种饿汉行为。对应在我们的计算机中如果打开硬盘上的文件,读取文件的内容,并显示内容的话,饿汉模式下就是把文件中所有的内容都读取到内存中,并显示出来。

代码展示:

package Thread;
//将这个类设置成单例的
class Singleton{
    //唯一实例的本体
    //这里Singleton被static修饰表示该属性是类的属性
    //JVM中每个类的类对象只有唯一一份,类对象里的这个成员也就自然只有唯一一份了
    private static Singleton instance = new Singleton();

    //获取到实例的方法
    public static Singleton getInstance() {
        return instance;
    }

    //禁止外部new实例,将构造方法设为私有的
    private Singleton() {

    }
}
public class ThreadDemo21 {
    public static void main(String[] args) {
        //此时 s1 和 s2是同一个对象!!!
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
//        Singleton s3 = new Singleton();
        System.out.println(s1 == s2);
    }
}

结果展示:
注意:

  • 我们是直接在类内就将实体定义好了,并且是由static来修饰的,这样就保证了该属性是类的属性,而且只要一份。
  • 我们要想在类外禁止掉新创建实体,那么我们就需要在类内将构造方法设置为私有的,这样就可以保证我们在类外不能创建实体了。
  • 这里可能就有同学好奇了,那么private修饰了构造方法,那么为什么上面可以创建一个实体而在主函数中就不可以创建了呢?还记得private的范围吗?由private修饰是不可以在类外访问而类内还是可以访问到的。这里大家一定要注意。
  • 我们在上述代码的结果中可以看到通过类来创建的两个对象其实指向的都是同一个空间,也就相当于是同一个对象了。

1.2懒汉模式

根据上面对饿汉模式的讲解相信大家对懒汉模式也大体有一个概念了,懒汉模式可以理解为“从容”,就是饿汉模式的反例,拿上述洗碗来举例子就是吃完饭之后不洗碗,等到下一次吃饭的时候在洗碗,对应到计算机中的那个读取文件内容的例子来说就是只读取文件中的一小部分,把当前屏幕填充上,如果用户翻页了,在读取其他文件的内容,如果不翻页就省下来了。

其实相比较上述的饿汉模式来说,懒汉模式的效率是比饿汉模式的效率高的。

下面我们用懒汉模式来给大家实现一下单例。

代码展示:

package Thread;
//通过懒汉模式来实现单例模式
class SingletonLazy{
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    //禁止类外创建实体
    private SingletonLazy() {

    }
}
public class ThreadDemo22 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}


结果展示:

相比上述的饿汉模式来说,懒汉模式其实就是在创建实体的时候作出了变动,在饿汉模式中是直接创建出了一个实体,而在懒汉模式中则是先查看有没有该对象的实体,如果有就不创建了,如果没有再去创建一个实体。

1.3结合线程安全下的单例模式

在上述中给大家简单的交代了什么是饿汉模式什么是懒汉模式,以及分别给大家用代码来实现了一下,接下来我们就结合这上节中给大家讲解的线程安全,来让单例模式也变得安全起来。如果没有查看线程安全的同学记得先查看一下线程安全这节博客哦!(http://t.csdn.cn/L0JJ6

首先我们先来看饿汉模式。

在饿汉模式中我们就只涉及到了读操作,所以它本身就是线程安全的,不需要修改。

下面看一下懒汉模式。

那么在懒汉模式中我们涉及到了一步判断,在执行判断的时候,如果是在多线程的情况下可能就处于一种不安全的状态了,如下所示:

如果t1和t2线程是按照上述的顺序执行的,当t1进入判断的时候是null的,此时当t2也进入判断的时候发现也是空的,那么在t1创建完实体之后,t2也会创建一个实体,此时不就多创建出来一个实体吗?这就与我们之前的预期结果不符了,就出现bug了。所以此时的线程是不安全的。那么在上节中我们出现这种问题是如何解决的呢?那当然是给线程加锁了。我们直接给类对象加锁,这样就可以保证判定和new是一个原子操作了。

如下所示:

但是加锁其实是一个低效的操作,我们在一般情况下是“非必要,不加锁”,因为加锁就有可能涉及到阻塞等待。

在上述代码中任何情况下调用getInstance都会出发锁的竞争。

其实这里的线程不安全只会出现在第一次创建对象这里,一旦对象new好之后,后续调用getInstance就只是单纯的读操作,就没有线程不安全问题了,也就没必要加锁了。

所以改进之后,如下面的代码所示:

解决了上述线程不安全的问题之后,我们还要注意一个很重要的问题就是指令重排序的问题。

假设两个线程同时调用getInstance的时候,就有可能出发指令重排序的问题。指令重排序的时候会有三步:

  1. 创建内存。
  2. 调用构造方法。
  3. 把内存地址赋给引用。

 其中的2 和 3 是可以随意调用的,顺序是可以颠倒的。

这里可以给大家举一个例子。在买房子的时候,我们是先看好房子之后,交首付,然后商家给你钥匙,此时你拿到的就是一个毛坯房,你自己去装修,亦或者是,商家已经将房子装修好了,直接给你钥匙,此时你拿到的就是一个精修房。

类比到我们计算机中就是创建内存就是买房子的操作,调用构造方法你可以想象成是装修房子,把内存赋给引用就是拿到了房子的钥匙

所以如果t1在执行完创建内存后先执行的是将内存赋给引用,此时系统调度给了t2了,然后t2进入if语句判断的时候发现是非空的,就直接返回t1的那个引用了,此时t2就相当于拿到的是一个毛坯房,接下来t2在调用里面的构造方法啥的可能就会出现一系列的问题!!!

此时我们就需要用到上节博客中给大家提到的防止指令重排序的一个关键字volatile,使用他我们就可以使得指令禁止重排序了。如下所示: 

代码整体展示:

package Thread;
//通过懒汉模式来实现单例模式
class SingletonLazy{
    //加关键字volatile 禁止指令重排序。
    volatile private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        //这个条件是判断是否要加锁,如果对象已经有了,就不必在加锁,此时本身线程就是安全的。
        if (instance == null) {
            //加锁
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    //禁止类外创建实体
    private SingletonLazy() { }
}
public class ThreadDemo22 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

1.4单例模式总结

单例模式的实现有饿汉模式和懒汉模式,同时单例模式下会存在线程安全问题。

饿汉模式:天然就是安全的,只是涉及到了读操作。

懒汉模式:不安全的,有读也有写,所以为了解决它我们分为了三步。

  1. 加锁:把if和new变成原子操作。
  2. 双重if,减少不必要的加锁操作。
  3. 使用volatile禁止指令重排序,保证后续的线程肯定拿到的是一个完整的对象。

2.阻塞式队列

2.1什么是阻塞队列

队列在之前的数据结构中大家都有了解过,就是一种先进先出的结构。阻塞队列是一种特殊的队列,也遵守着“先进先出”的原则。

阻塞队列是一种线程安全的数据结构,并且具有以下特性:

  • 如果队列空,尝试出队列,就会阻塞等待,等待队列不空为止。
  • 如果队列满,尝试入队列,就会阻塞等待,等待队列不满为止。

先来给大家介绍一下阻塞队列中的两个核心方法:
代码展示:

package Thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        //这里的BlockingDeque是一个接口所以在实例化的时候我们使用的是以下两种形式
        //可以是数组形式的
//        BlockingDeque<String> queue = new ArrayBlockingQueue<>();
        //也可以使用的是链的形式
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        //阻塞队列的核心方法,主要有两个
        //1.put入队列
        queue.put("hello1");
        queue.put("hello2");
        queue.put("hello3");
        queue.put("hello4");
        queue.put("hello5");

        //2.take出队列
        String result = null;
        result = queue.take();
        System.out.println(result);

        result = queue.take();
        System.out.println(result);

        result = queue.take();
        System.out.println(result);

        result = queue.take();
        System.out.println(result);

        result = queue.take();
        System.out.println(result);
    }
}


结果展示:

上述是执行了5次put操作5次take操作。发现一切都是正常的,但是如果我们执行5次put操作,6次take操作呢?如下代码所示:
代码展示:

package Thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        //这里的BlockingDeque是一个接口所以在实例化的时候我们使用的是以下两种形式
        //可以是数组形式的
//        BlockingDeque<String> queue = new ArrayBlockingQueue<>();
        //也可以使用的是链的形式
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();
        //阻塞队列的核心方法,主要有两个
        //1.put入队列
        //第一次put操作
        queue.put("hello1");
        //第二次put操作
        queue.put("hello2");
        //第三次put操作
        queue.put("hello3");
        //第四次put操作
        queue.put("hello4");
        //第五次put操作
        queue.put("hello5");

        //2.take出队列
        String result = null;
        //第一次take操作
        result = queue.take();
        System.out.println(result);
        //第二次take操作
        result = queue.take();
        System.out.println(result);
        //第三次take操作
        result = queue.take();
        System.out.println(result);
        //第四次take操作
        result = queue.take();
        System.out.println(result);
        //第五次take操作
        result = queue.take();
        System.out.println(result);
        //第六次take操作
        result = queue.take();
        System.out.println(result);
    }
}


结果展示:


jconsole所示:

可以通过结果或者是直接通过jconsole来看到线程是处于一个阻塞状态的!!!

2.2生产者消费者模型

在阻塞队列中一个典型的应用场景就是“生产者消费者模型”,这是一种典型的开发模型。

这里给大家举一个例子:
包饺子的场景。

包法1:每一个滑稽老铁,自己擀一个饺子皮,自己包一个饺子,在擀一个饺子皮,在包一个饺子。

虽然上述做法,确实是每一个滑稽都包饺子了,但是如果有一个擀面杖的话这个效率显然就很低!这就相当于四个滑稽老铁需要同时竞争一个擀面杖。所以我们更常见的包法是第二种包法,流水线的包法。 

包法2:一个滑稽负责擀饺子皮,另外三个负责包饺子。

上述情况下就构成了生产者和消费者模型,对于饺子皮来说, A滑稽就是一个生产者,他负责生产饺子皮,而B、C、D滑稽就是消费者,他们负责消费饺子皮。

在生产者和消费者之间,需要数据的交互,此时就需要一个交易场所

在上述的包饺子的场景下,此时如果A擀好了饺子皮,要给B、C、D就需要用一个东西先盛着,比如我们家里常用到的盖帘。此时这个盖帘就相当于是我们这里的交易场所

概念:生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不再等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

这里提到了一个概念叫“耦合”,不知道大家之前有没有听说过“高内聚,低耦合”,下面就给 大家分别解释一下“耦合”和“内聚”的概念吧!

耦合:是指两个模块之间的关联关系是强还是弱,关联越强耦合越高,关联越低耦合越低。

  • 举个例子:比如说是家里人如果生病了,那么此时我们就必须放下手头的工作去照顾家人,但是如果是你多年不联系的朋友生病了,那么我们最多就是多嘱咐他几句,让他好好休息,所以对于家人生病来说对我们的影响是比较大的,但是对于多年不联系的朋友来说对于我们的影响就比较小了,所以计算机中我们追求的是低耦合,避免代码牵一发而动全身。

内聚:相关联的代码,有没有放在一起,如果相关联的东西没有放在一起,随便乱放那就是低内聚,如果相关联的代码分门别类的规制起来就是高内聚。

  • 举个例子:如果你的衣服在家里是随便乱放,并且还不按照春夏秋冬保管起来,回家后衣服也就随随便便扔在一个角落,当你下次在找衣服的时候就会很麻烦,可能就需要找很久,这就是低内聚,而如果你的衣服是按照春夏秋冬分类放置,并且回家后衣服也是由固定的位置,鞋、衣服、袜子...都有专门的位置来摆放,那么你在寻找的时候就会非常方便,此时就是高内聚。所以我们在代码管理的时候也需要做到高内聚,将同一类的代码放置在一起,这样也方便后期的管理和应用。

2.2.1 上下游模块之间进行“解耦合”

那么了解了什么是“高内聚,低耦合”之后,在回到我们的生产者消费者模型这块,在上面给大家强调在生产者消费者模型中是要降低他两之间的耦合性,也就是“解耦合”,那么我们是如何做到解耦合的呢?就是小编在上述包饺子中举例子中的那个“盖帘”,通过这个“盖帘”我们就可以很好的降低它两之间的耦合性了,这里的盖帘在我们计算机中是有一个专门的数据结构来扮演的“阻塞式队列”。

比如我们先来考虑以下这个场景:

A服务器调用B服务器(A给B发送请求B给A返回响应)。

此时如果A和B直接通信,此时就是耦合比较高的情况。如果B挂了,对于A会有之间的影响,A也就跟着挂了。另外如果要再加个C,此时对于A就有一个比较大的调整。这就是高耦合的情况。那么我们自然是不希望在现实生活中出现这种情况。 所以就引入了生产者消费者模型,耦合就降低了,如下所示:

如上图所示,中间有一个阻塞队列服务器就会降低耦合性了,此时的A不知道B和C的存在,B和C也不知道A的存在,他们就只认识队列,此时的B挂了对于A来说是没有影响的。 

2.2.2削峰填谷

为了避免耦合性高的情况一种就是上述给大家讲解的通过中间的“阻塞队列服务器”来进行解耦合,另一种就是“削峰填谷”。考虑以下场景:

A收到的请求数量,是和用户的行为相关的,但是用户行为是随机的,有的情况下,请求会出现“峰值”突然爆发式增长,比如我们在学校强选修课的时候,所以如果是上述图示所示的情况下,当A收到一个峰值的同时,B也就会收到一个峰值,但是如果B设计的时候没有考虑到峰值处理的情况,可能就会挂掉,这就给系统的稳定性带来了风险。

解决方案:

 

此时A收到的请求多了,队列中的元素也就会越来越多,此时B任然可以按照之前处理数据的速度来处理队列中的数据,所以也就是队列帮助B承担了压力,此时B就不容易挂掉,也就达到了我们所说的“削峰填谷”的效果。这就相当于是我国建造的“三峡大坝”当遇到洪水的时候,大坝就起到了一个缓冲的效果,让下游的水不至于一下子太多,从而发生重大的人财损失,但是如果遇到干旱季节,大坝就会将之前储蓄的水在放出来,也不至于下游太干旱。 

 下面我们就用代码来实际给大家来演示一下吧。

代码展示:

package Thread;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class ThreadDemo24 {
    public static void main(String[] args) {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                int value = 0;
                try {
                    value = blockingDeque.take();
                    System.out.println("消费元素:" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();

        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产元素:" + value);
                    blockingDeque.put(value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();

        //上述代码是让生产者每隔1s生产一个元素。
        //让消费者直接消费,不受限制
    }
}

结果展示:

2.3阻塞队列的实现

下面我们来自己实现一下阻塞队列:

实现阻塞队列我们分三步走:

  1. 先实现一个普通队列。
  2. 加上线程安全。
  3. 加上阻塞功能。

代码展示:

package Thread;
//基于数组实现队列
class MyBlockingQueue{
    private int[] items = new int[1000];
    //约定在[head,tail)为队列的有效元素
    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private int size = 0;

    //入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            //队列已满
            // return;
            this.wait();
        }
        //把新元素放到tail所在的位置上
        items[tail] = elem;
        tail++;
        //万一tail达到了上限,就需要让tail从头开始
        if (tail == items.length){
            tail = 0;
        }
        //tail = tail % items.length
        size++;
        this.notify();
    }

    //出队列
    synchronized public Integer take() throws InterruptedException {
        while (size == 0){
//            return null;
            this.wait();
        }
        int value = items[head];
        head++;
        if (head == items.length) {
            head = 0;
        }
        size--;
        this.notify();
        return value;
    }
}
public class ThreadDemo25 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费:" + value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产:" + value);
                    queue.put(value);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();

        System.out.println("hello");
    }
}


结果展示:

结束语:

这节中小编主要是与大家分享了设计模式中的两个重要的模式,单例模式和阻塞式队列,还给大家实现了一下阻塞式队列的代码,希望这节对大家学习JavaEE有一定的帮助,想要学习的同学记得关注小编和小编一起学习吧!如果文章中有任何错误也欢迎各位大佬及时为小编指点迷津(在此小编先谢过各位大佬啦!)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力敲代码的小白✧٩(ˊωˋ*)و✧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值