多线程案列

一.单例模式

单例模式能保证某个类在程序中只存在唯一 一份实例, 而不会创建出多个实例.单例模式具体的实现方式, 分成 “饿汉”“懒汉” 两种.

1.1饿汉模式

在这里插入图片描述
类加载阶段,就把实例创建出来,被称为饿汉模式

1)static 保证这个实例唯一
2)static 保证这个实例在一定时机中被创建出来

//此处保证Singleton这个类只能创建出一个实例
class Singleton{
    
    
    //在此处,先把这个实例给创建出来
    private static Singleton instance = new Singleton();
    //如果需要使用这个唯一实例,统一通过Singleton.getInstance()方式来获取
    public static Singleton getInstance(){
    
    
        return instance;
    }
    //为了避免Singleton类不小心被复制出多份
    //把构造方法设为private.在类外面,就无法通过new的方式来创建这个Singleton实例
    private Singleton(){
    
    

    }
}

public class Thread20 {
    
    
    public static void main(String[] args) {
    
    
        Singleton s = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s == s2);
    }

}

结果:true

1.2 懒汉模式

在这里插入图片描述 上述写的饿汉模式和懒汉模式,在多线程环境下调用getInstance.可以发现饿汉模式是线程安全的,而懒汉模式是不安全的

饿汉模式多线程调用,只涉及到"读操作",线程是安全的

懒汉模式的执行过程
在这里插入图片描述
此时加锁
在这里插入图片描述

但是导致每次getInstance 都需要加锁
一旦对象new完,后续调用getInstance,此时instance的值一定是非空的,因此就会直接触发return

基于上述讨论,就可以给上面的代码加上一个判定,如果对象还没创建,才加锁
如果对象已经创建过了,就不加锁
代码如下:
在这里插入图片描述
上述的代码仍存在问题
在这里插入图片描述

instance=new Singleton()
拆成三个步骤

  1. 申请内存空间
  2. 调用构造方法,把这个内存空间初始化成一个合理的对象
  3. 把内存空间的地址赋值给instance引用
    正常情况下,是按照123这个顺序来执行的
    编译器还有优化操作,指令重排序
    为了提高程序效率,调整代码执行顺序
    123这个顺序就可能变成132(如果是单线程,123和132没有本质区别)

假设
t1是按照132的步骤执行的
t1执行的13之后,执行2之前,被切出cpu
t2来执行(当t1执行完3之后,t2看起来,此处的引用就非空了)
此时此刻,t2就相当于直接返回了instance引用
并且可能会尝试使用引用中的属性

但是由于t1中的2(装饭)操作还没执行完,t2拿到的是非法的对象,还没构造完成的不完整对象
这就需要volatile

1.解决内存可见性
2.禁止指令重排序

完整代码如下:

class SingletonLazy{
    
    
    private volatile 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 Thread21 {
    
    
    public static void main(String[] args) {
    
    
        SingletonLazy s = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s == s2);
    }

二.阻塞队列

阻塞队列是一个队列,先进先出,不过,是特殊的队列,带有特殊的功能阻塞
1.如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里添加元素(队列不空为止)
2.如果队列满了,执行入队操作,同样会阻塞,阻塞到另一线程从队列取走元素(队列不满)
阻塞队列的一个典型应用场景就是"生产者消费者模型"

2.1生产者消费者模型

先来看一个典型场景
在这里插入图片描述

上述场景中,A和B之间的耦合是比较高
A要调用B,A务必要知道B的存在,如果B挂了,很容易引起A的bug,此外,如果在加一个C服务器,A也要修改不少代码

针对上述场景,使用生产者消费者模型,就可以有效的降低耦合
在这里插入图片描述

此时,A和B之间的耦合度就降低很多
A不知道B的存在,A只知道队列(A的代码没有任何一行代码和B相关)
B不知道A的存在,B只知道队列(B的代码没有任何一行代码和A相关)

如果B挂了,对于A没有任何影响,因为队列还好着,A仍然可以给队列插入元素,如果队列满,就会阻塞
同理,A挂了,B也没影响,B仍可以从队列获取元素,等到队列为空,就先阻塞.
生产者消费者模式,带来的好处:
1.实现了发送方和接收方之间的"解耦"
2.可以做到"削峰填谷",保证系统的稳定性

2.2标准库提供的阻塞队列

先来认识一下接口:
在这里插入图片描述
实现类:在这里插入图片描述
阻塞队列主要方法是两个

1.入队列,put
2.出队列,take
都带有阻塞功能

相关代码

public class Thread23 {
    
    
    public static void main(String[] args) {
    
    
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
        //创建两个线程,来作为生产者和消费者
        Thread customer = new Thread(()->{
    
    
            while (true) {
    
    
                try {
    
    
                    Integer result = blockingQueue.take();
                    System.out.println("消费元素: " + result);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }

            }
        });
        customer.start();
        Thread producer = new Thread(()->{
    
    
            int count = 0;
            while (true){
    
    
                try {
    
    
                    blockingQueue.put(count);
                    System.out.println("生产元素 : "+ count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();

    }
}

2.3实现阻塞队列

2.3.1普通队列

class MyBlockingQueue{
    
    
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    public void put(int value){
    
    
        if(size == items.length){
    
    
            return;
        }
        items[tail] = value;
        tail++;
        //1)
//        tail = tail % items.length;
        //2)
        if(tail >= items.length){
    
    
            tail = 0;
        }
        size++;
    }
    public Integer take(){
    
    
        if(size == 0){
    
    
            return null;
        }
        int result = items[head];
        head++;
        if(head >= items.length){
    
    
            head = 0;
        }
        size --;
        return result;
    }
}

判断队列是空还是满
1)浪费一个元素
2)引入一个size,记录个数

2.3.2加上阻塞功能

class MyBlockingQueue{
    
    
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;
//    入队列
    public void put(int value) throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while (size == items.length){
    
    
                //队列满了,此时要产生阻塞
                this.wait();
            }
            items[tail] = value;
            tail++;
            //tail的处理
            //tail = tail % items.length;
            if(tail >= items.length){
    
    
                tail = 0;
            }
            size++;
//            这个notify唤醒take中的wait
            this.notify();
        }

    }
    //出队列
    public Integer take() throws InterruptedException {
    
    
        int result = 0;
        synchronized (this) {
    
    
            while (size == 0){
    
    
                //队列空,也应该阻塞
                this.wait();
            }
            result = items[head];
            head++;
            if(head >= items.length){
    
    
                head = 0;
            }
            size--;
            //唤醒put中的wait
            this.notify();
        }
        return result;
    }

}

在这里插入图片描述

两个线程中的wait不可能同时触发

在这里插入图片描述

take的wait同理

三.定时器

类似于一个"闹钟",达到一个设定的时间之后就执行某个指定好的代码
当网络编程的时候,可以使用定时器,来进行"止损"

3.1标准库中的定时器

标注库中提供了一个Timer类.Timer类的核心方法为schedule
schedule 方法 包含两个参数.第一参数时间到要执行的任务代码,第二个参数指定多长时间之后执行
这里是引用
具体代码

public class Thread25 {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("程序启动!");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("运行定时器任务1");
            }
        },3000);
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("运行定时器任务2");
            }
        },2000);
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("运行定时器任务3");
            }
        },1000);
    }
}

结果

这里是引用

猜你喜欢

转载自blog.csdn.net/weixin_63993025/article/details/129799144
今日推荐