多线程初阶(三)单例模式及阻塞队列

目录

前言:

单例模式

饿汉模式

代码实现

懒汉模式

代码实现

阻塞队列

生产者消费者模型

标准库中阻塞队列使用(实现生产者消费者模型)

模拟实现阻塞队列

代码实现

小结:


前言:

    这篇主要介绍一些多线程的使用案例,使用多线程的编程方式解决一些实际问题。在多线程的代码编写里,一定要注意线程安全问题,及其一些其他内存可见性等问题。

单例模式

    所谓单例模式,就是通过语法的结构,使一个类只能实例出一个对象。不管在什么情况下,这个对象始终是同一份,即它们引用所指向的内存空间也是同一块空间。

饿汉模式

    所谓饿汉模式,就是在类加载的时候,就已经实例化了这个对象,我们只需要对外提供获得这个实例的方法即可。为了确保这个类只能实例出一个对象,就可以将它的构造方法设置为私有。类外不能够调用构造方法,即类外也就无法实例化对象。

代码实现

public class Singleton {
    //static修饰属于类属性,在类加载(解析class文件)时就创建好了,只有唯一一份,同时也就只有这一个对象
    private static Singleton singleton = new Singleton();

    //只能通过这个方法获取实例
    public static Singleton getInstance() {
        return singleton;
    }

    //将构造方法设为私有
    private Singleton() {};

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

    注意:多线程情况下,当我们调用getInstance获取这个类的实例时,只涉及到读取singleton这个实例的内存地址。那么天然就是线程安全的。

懒汉模式

    所谓懒汉模式,就是在调用获取类实例方法时在实例化对象,相比于饿汉模式就显得没有那么急切。还是一样将它的构造方法设置为私有。

代码实现

public class SingletonLazy {
    private volatile static SingletonLazy singletonLazy = null;
    //在实际调用方法时实例化对象
    public static SingletonLazy getInstance() {
        if(singletonLazy == null) {
            synchronized (SingletonLazy.class) {
                if(singletonLazy == null) {
                    singletonLazy = new SingletonLazy();//指令重排序
                }
            }
        }
        return singletonLazy;
    }
    private SingletonLazy() {};
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
        System.out.println("aaaa");
    }
}

注意:

    获取实例的SingletonLazy 方法里涉及写内存和读内存,那么在多线程环境下就会出现线程不安全情况。线程不安全情况就是因为可能两个线程读取到内存中的值是一样的,对于代码来说就有可能都读取singletonLazy为null,对于里面if而言,就会实例两次。

    解决方法,在里面的if外加锁,保证if块里代码的原子性,那么当第一个线程读取的时候,另一个线程就会阻塞等待,直到上一个线程修改完成,这个线程才会取读取singletonLazy,这样就有效的避免了上述情况。

    加锁对于cpu是有一定消耗的,会影响代码的执行效率。这里只有当两个线程同时读取singletonLazy为null时,才会产生线程安全问题。如果读取到已经不为null,那么就不需要加锁,直接返回这个对象的引用即可。代码里就是最外层if。

    由于SingletonLazy 方法里涉及写内存和读内存,那么对于singletonLazy 变量就存在内存可见性问题。如果编译器认为这个变量是不可变的,当在代码里已经修改这个变量,但是编译器始终只读取cpu寄存器上的值,那么就会导致读取到的值和内存中的值不同步。volatile 关键字就是声明这个变量是可变的,编译器每次读取值的时候,先去内存中读取,这样就不会存在不同步的情况。

    singletonLazy = new SingletonLazy();这句代码涉及指令重排序。这句代码涉及三个大步骤:1.申请内存。2.调用构造方法,实例这块内存。3.返回内存地址。如果在单线程环境下,不论怎样都是没有问题的。多线程环境下如果先执行了1,3就被调度走了,那么获得这个内存就是不完整的。volatile关键字可以禁止对其进行指令重排序。

阻塞队列

特性:

    1)如果队列为空,出队列就会阻塞,阻塞到队列不为空为止。

    2)如果队列满了,入队列就会阻塞,阻塞到队列不满为止。

    基于阻塞队列提出了消息队列,在每个数据中加上”消息类型“的标签,按照类型进行先进先出的模式。消息队列已经被一些大佬写成了程序,部署在一组服务器上,我们就可以通过客户端的方式发请求,使用消息队列。

由于阻塞队列的特点可以实现生产者消费者模型。

生产者消费者模型

特性:

    1)实现发送方和接收方之间的解耦和。

    2)做到”削峰填谷“,保证系统的稳定性。

解释:

    如果两个服务器之间直接进行调用,那么其中一个服务器挂了,就会影响其他的服务器,耦合性太高。在其之间加上阻塞队列(消息队列),发送方服务器先把数据入到阻塞队列中,然后接收方服务器直接从阻塞队列中取数据即可。这样就算接收方服务器挂了,但是阻塞队列还存在,就不会直接影响发送方服务器。这样就降低了服务器之间的耦合程度。并且服务器中的代码也只需要针对阻塞队列即可,方便后期的维护。

    如果某一时刻,请求量大量增加。如果服务器和服务器是直接调用,就可能存在服务器崩掉的情况。那么在其之间加上阻塞队列,就算请求量大量增加,当阻塞队列满的时候就会阻塞,后面的服务器也是照常处理数据。阻塞队列就可以起到一个缓冲的作用。

标准库中阻塞队列使用(实现生产者消费者模型)

public class ThreadDemo25 {
    public static void main(String[] args) {
        BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
        //生产者
        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                System.out.println("生产:" + count);
                try {
                    blockingQueue.put(count);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            }
        });
        //消费者
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    Integer tmp = blockingQueue.take();
                    System.out.println("消费:" + tmp);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

    注意:t1线程作为生产者不断入数据,t2线程作为消费者,不断取数据。BlockingQueue中只有put方法和take方法带有阻塞功能,入数据和出数据。

模拟实现阻塞队列

    队列可以基于链表,数组,优先级队列实现。这里采用循环数组的方式实现。

    循环数组,用head和tail记录有效数据的区间。所谓循环数组,就是当数据入到最后一个位置时就可以在前面空位置继续入数据,这里只需要对head和tail进行处理即可。

代码实现

class MyBlockingQueue {
    private int[] elem;
    int head = 0;
    int tail = 0;
    int size = 0;
    public MyBlockingQueue() {
        this.elem = new int[1000];
    }
    public void put(int value) throws InterruptedException {
        //队列满了,如果再如数据就阻塞,阻塞到队列不满为止
        //虽然notify通知之后,队列就不满了,为了防止通知了队列任然是满的,这里用while循环
        //这个方法及右读又有写,多线程环境下是不安全的,那么就需要保证这些代码的原子性
        synchronized (this) {
            while (elem.length == size) {
                this.wait();
            }
            elem[tail] = value;
            tail++;
            //tail = tail % elem.length;
            if(tail >= elem.length) {
                tail = 0;
            }
            this.size++;

            //唤醒take中的wait
            this.notify();

        }
    }
    public Integer tack() throws InterruptedException {
        //如果队列空了,再出数据就阻塞,阻塞到队列不空为止
        //虽然notify通知之后,队列就不为空了,为了防止通知了队列任然是空的,这里用while循环
        int tmp = 0;
        synchronized (this) {
            while (this.size == 0) {
                this.wait();
            }
            tmp = elem[head];
            head++;
            if(head >= elem.length) {
                head = 0;
            }
            size--;
            //唤醒put中的wait
            this.notify();
        }
        return tmp;
    }
}
public class ThreadDemo22 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        myBlockingQueue.put(1);
        myBlockingQueue.put(2);
        myBlockingQueue.put(3);
        System.out.println(myBlockingQueue.tack());
        System.out.println(myBlockingQueue.tack());
        System.out.println(myBlockingQueue.tack());
        myBlockingQueue.tack();
    }
}

    注意:这里根据阻塞队列的特性,入数据满了就需要阻塞,这里调用wait方法,把锁加到this对象上,因为只有对同一个对象中多线程访问才会产生问题。当队列不满时,即出数据了,阻塞就停止了,进入就绪队列。这里在take方法里出完数据就调用notify方法通知put方法阻塞结束。

    当阻塞结束时,队列真的就是不满的了么,这里可能会存在一些其他问题。为了代码足够稳妥,这里使用while循环,通知之后再进行判断。

    这里的put方法里涉及了读和写数据,多线程情况下就会存在线程不安全情况,即对这块代码加锁,保证其原子性。其他线程访问时就阻塞等待。tack方法的原理和put方法一致。

小结:

    写多线程代码时,要考虑线程的抢占式执行,随机调度带来的线程安全问题,及其一些其他多线程可能存在的问题。

你可能感兴趣的:(JavaEE,单例模式,java,开发语言)