[JavaEE] Multi-threaded code example: singleton mode and blocking queue BlockingQueue

Table of contents

Singleton mode:

What is the singleton pattern? 

Implementation of the singleton pattern: 

Hungry man mode: 

Lazy mode: 

Discussion on thread safety issues of singleton mode based on concurrent programming: 

Blocking queue:

Blocking queues in the standard library: 

Self-implementing blocking queue: 

Producer consumer model: 

Implementation of the producer consumer model: 

Use the system's BlockingQueue to implement the producer-consumer model:

Implement the producer consumer model with self-implemented BlockingQueue:


 

Singleton mode:

What is the singleton pattern? 

The singleton pattern can guarantee that only one instance of a certain class can exist, and multiple instances cannot be created. This design pattern needs to be used in specific business scenarios.

Implementation of the singleton pattern: 

There are many ways to implement the singleton mode, the main ways are the hungry man mode and the lazy man mode

Hungry man mode: 

A simple implementation of the lazy man pattern:


//单例模式:饿汉模式

class Singleton{
    //第一步实例化对象
   public static Singleton singleton=new Singleton();
    //构造方法为空
    private Singleton(){}
    //获取对象实例
    public static Singleton getInstance(){
        return  singleton;
    }
}


public class ThreadDemo4 {
    public static void main(String[] args) {
        //Singleton singleton1=new Singleton();无法创建对象!!!
        //验证单一性:
        Singleton singleton1=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
        System.out.println(singleton1==singleton2);


    }
}

Through comparison, we found that the obtained instance is the same instance, and the instance can no longer be created in this mode. From the code, we can know that the creation of instances of this pattern is earlier than the creation of instances of general classes, so we vividly call it the hungry man pattern (I can’t wait for the hunger), and the instance of this object is created during the class loading phase. .

How does the hungry man mode ensure that the created object is a singleton? Create a static object + private constructor when defining a class, expose the interface get instance, and set it as static to ensure that it can be called directly by using the class name. 

Lazy mode: 

The reason why the lazy man mode is called this is also a very vivid statement. The singleton mode in this mode will only create an instance when an instance is needed, and only this time will be created.

Lazy mode is simple to implement:


class Singleton1{
    public static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public static Singleton1 getSingleton1() {
        if(singleton1==null){
            singleton1=new Singleton1();
        }
        return singleton1;
    }
}

public class ThreadDemo5 {
    public static void main(String[] args) {
        //Singleton1 singleton1=new Singleton1();无法创建实例
        Singleton1 s1=Singleton1.getSingleton1();
        Singleton1 s2=Singleton1.getSingleton1();
        System.out.println(s1==s2);

    }
}

  

Obviously, our instance is created when the instance is acquired for the first time. 

Lazy mode is achieved by creating static object variables + creating objects when needed + providing public interfaces and setting them as static methods and private constructors.

Note here: the above-mentioned singleton mode has no security problems when running in single-threaded mode, but it will cause problems in concurrent programming! ! !

Discussion on thread safety issues of singleton mode based on concurrent programming: 

 We can see: in the hungry man mode, we instantiate the object as soon as we come up, and there will only be read operations in multi-threading, so there will be no thread safety issues, so we say singleton in the hungry man mode Patterns are thread-safe. But for the lazy man mode, the instance is created when the instance is obtained, which involves both reading and writing operations.

The thread safety problem occurs when the instance is created for the first time. If the getInstance method is called in multiple threads at the same time, it may cause
multiple instances to be created. Once the instance has been created, it is no longer thread safe to call getInstance in a multi-threaded environment later The problem is gone (singleton1 is no longer modified),

so using synchronized can improve the thread safety problem here

Lazy mode multi-threading improved version 1.0:


class Singleton1{
    public static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public synchronized static Singleton1 getSingleton1() {
        if(singleton1==null){
            singleton1=new Singleton1();
        }
        return singleton1;
    }
}

But, do you think this is the end? NO! ! !

There are still some problems here! Such as lock competition, memory visibility issues and so on. Locking/unlocking is a relatively expensive thing. The thread insecurity of the lazy mode only occurs when the instance is first created. Therefore, there is no need to lock it for subsequent use. So we consider using an if to determine whether the singleton1 instance has been created. At the same time, in order to avoid the "memory visibility" from causing deviations in the read singleton1, volatile is added. When multiple threads call getInstance for the first time, everyone may find that the instance is null, so they continue to execute to compete for the lock, and the thread that successfully competes completes the operation of creating the instance. After the instance is created, we need to use an if to judge whether the creation is completed. If the creation is completed, other threads competing for the lock will be blocked by the if of this layer, and other instances will not be created.

So we will make a second improvement to the lazy man mode:

Lazy mode multi-threading improved version 2.0:


class Singleton1{
    public volatile static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public  static Singleton1 getSingleton1() {
        if(singleton1==null){
            synchronized (Singleton1.class){
                if(singleton1==null){
                    singleton1=new Singleton1();
                }
            }
        }
        return singleton1;
    }
}

In this way, our lazy model is perfect.

The following code has made further changes on the basis of locking:
use double if judgments to reduce the frequency of lock competition, and add volatile to singleton1 Let
us give an example :

1) There are three threads, start to execute getInstance, and know the message that the instance has not been created through the outer if (singleton1 == null), so they start to compete for the same lock 2) Among them,
thread 1 acquires the lock first, and thread 1 at this time Use the if (singleton1 == null) in the inner layer to further confirm whether the instance has been created. If not, create the instance. 3)
When thread 1 releases the lock, thread 2 and thread 3 also get the lock, and also through the inside layer if (singleton1 == null) to confirm whether the instance has been created, and if the instance has been created, it will not be created again
4) Subsequent threads do not need to be locked, directly through the outer layer if (singleton1 == null) You know that the instance has been created, so you don't try to acquire the lock anymore, which reduces the overhead.
Try to understand it.

Blocking queue:

A blocking queue is a special queue that also follows the "first in, first out" principle.
A blocking queue can be a thread-safe data structure, and has the following characteristics:
when the queue is full, continuing to enter the queue will block until other threads take elements from the queue.
When the queue is empty, continuing to exit the queue will also block until other threads insert elements into the queue.
A typical application scenario
of blocking queue is " producer consumer model ". This is a very typical development model 

Blocking queues in the standard library: 

Blocking queues are built into the Java standard library.. If we need to use blocking queues in some programs, we can directly use the ones in the standard library.

Classes and interfaces that implement blocking queue functionality are provided in the standard library. Although the blocking queue is essentially a queue, that is to say, the Queue interface is implemented, and there are also common queue methods, but we mainly use the blocking queue instead of these, but its unique blocking function. At this time, the corresponding enqueue and The methods of dequeue operation correspond to the put and take methods respectively .


At the same time, BlockingQueue also has these commonly used classes that implement the Queue interface. The data structure behind it can be known by its name. In addition, what is followed by Deque is a double-ended blocking queue. 

important point:

BlockingQueue is an interface, and the real implementation class is LinkedBlockingQueue.
The put method is used for blocking enqueue, and take is used for blocking dequeue.
BlockingQueue also has offer, poll, peek and other methods, but these methods do not have blocking features.

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueDemo1 {
    public static void main(String[] args) throws InterruptedException {
        //阻塞队列
        BlockingQueue<String> queue=new LinkedBlockingQueue<>();
        //入队列
        queue.put("abc");
        //出队列,如果没有就进行阻塞
        String elem=queue.take();
        System.out.println(elem);
    }
}

In the blocking queue, if the queue is full or there are no elements out, it will enter the blocking state. Here is a demonstration of the case of no elements:

 At this time, there are no elements in the queue, and the program is blocked.

Self-implementing blocking queue: 

 The key to implementing a blocking queue is to realize its blocking function. Others are similar to ordinary queues. The put and take methods are mainly implemented here:


class MyBlockingQueue{

    //利用数组实现
    private int[] arr=new int[1000];//设定数组长度为1000

    private int size=0;//记录数组的内容长度
    //利用end和begin两个指针使得数组变为循环数组(逻辑上的循环)
    private int end=0;
    private int begin=0;

    //实现put方法
    //阻塞考虑使用wait和notify进行唤醒(sleep不太靠谱)
    public void put(int value) throws InterruptedException {
        //判断是否满了(这里要用循环判断,因为在多线程当中,线程被唤醒的时候不一定不满)
        //加锁保证原子性
        synchronized (this){
            while(size>= arr.length){
                this.wait();
        }
            //不满之后放入元素
            arr[end]=value;
            //调整长度
            end++;
            size++;
            //如果放满了则将end变为0
            if(end>= arr.length){
                end=0;
            }
            //进行唤醒
            this.notify();
        }
    }
    //实现take方法
    public int take() throws InterruptedException {
        synchronized (this){
            //判断是否为空
            while (size==0){
                this.wait();
            }
            //不空之后开始取出元素
            int ret=arr[begin];
            begin++;
            if(begin>= arr.length){
                begin=0;
            }
            size--;
            this.notify();
            return ret;
        }

    }
    //长度
    public synchronized int Size(){
        return size;
    }

}


public class BlockingQueueDemo3 {

    public static void main(String[] args) throws InterruptedException {

        MyBlockingQueue queue=new MyBlockingQueue();
        queue.put(100);
        queue.put(200);
        queue.put(300);
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());

    }
}

 

 Obviously, when there are no elements in it, it will block and wait. 

Producer consumer model: 

The producer-consumer model uses a container to solve the problem of strong coupling between producers and consumers.
Producers and consumers do not communicate directly with each other, but communicate through blocking queues. Therefore, after producing data, producers do not need to wait for consumers to process it, but directly throw it to the blocking queue. Consumers do not ask producers for data, but Take it directly from the blocking queue.

First of all, the blocking queue is equivalent to a buffer, which balances the processing power between producers and consumers.

Secondly, the blocking queue enables decoupling between producers and consumers, that is, consumers no longer directly depend on producers.

For a blocking queue, the producer is the party that adds elements, the consumer is the party that takes the elements, and the product is the element of the blocking queue. Producers and consumers communicate with each other through blocking queues.

Draw a picture to demonstrate:

Let's take the example of krypton gold in the game! ! !

In this way, there will be a problem: the coupling between server A and server B is too high. Once there is a problem with one of the servers, it will cause the other server to be unable to complete the requirements. There will be a situation where one server hangs up and the other server is taken away.

Moreover, if we need to add a new server to participate in other related functions, such as logs, there will be problems!

So how to solve this situation? This uses the current producer-consumer model. We can put the resources generated by the producer into a blocking queue. When a consumer needs to consume, it will be taken directly from the blocking queue. If there is no resource in the queue, it will be blocked and wait for the producer. For production, when the blocking queue is full, the producer also blocks and waits. Server A here is the producer, and server BC is the consumer. So we can use this model to design something like this:

Implementation of the producer consumer model: 

Use the system's BlockingQueue to implement the producer-consumer model:

First, we use the BlockingQueue provided by the system to implement the producer-consumer model: 

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueDemo2 {
    public static void main(String[] args) {
        //创建阻塞队列
        BlockingQueue<Integer>queue=new LinkedBlockingQueue<>();
        //使用两个线程:一个线程充当生产者,一个线程充当消费者
        //生产者
        Thread t1=new Thread(()->{
            int count=0;
            while(true){
                try {
                    queue.put(count);
                    System.out.println("生产者生产:"+count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2=new Thread(()->{

            while(true){
                try {
                    int ret=queue.take();
                    System.out.println("消费者消费:"+ret);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

 

We can clearly see that consumption elements and production elements appear in pairs. This prevents the situation where what is not produced by the producer is consumed. 

Implement the producer consumer model with self-implemented BlockingQueue:


class MyBlockingQueue1{

    //利用数组实现
    private int[] arr=new int[1000];//设定数组长度为1000

    private int size=0;//记录数组的内容长度
    //利用end和begin两个指针使得数组变为循环数组(逻辑上的循环)
    private int end=0;
    private int begin=0;

    //实现put方法
    //阻塞考虑使用wait和notify进行唤醒(sleep不太靠谱)
    public void put(int value) throws InterruptedException {
        //判断是否满了(这里要用循环判断,因为在多线程当中,线程被唤醒的时候不一定不满)
        //加锁保证原子性
        synchronized (this){
            while(size>= arr.length){
                this.wait();
            }
            //不满之后放入元素
            arr[end]=value;
            //调整长度
            end++;
            size++;
            //如果放满了则将end变为0
            if(end>= arr.length){
                end=0;
            }
            //进行唤醒
            this.notify();
        }
    }
    //实现take方法
    public int take() throws InterruptedException {
        synchronized (this){
            //判断是否为空
            while (size==0){
                this.wait();
            }
            //不空之后开始取出元素
            int ret=arr[begin];
            begin++;
            if(begin>= arr.length){
                begin=0;
            }
            size--;
            this.notify();
            return ret;
        }

    }
    //长度
    public synchronized int Size(){
        return size;
    }

}


public class BlockingQueueDemo4 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue1 queue1=new MyBlockingQueue1();
        //生产者
        Thread producer =new Thread(()->{
            int count=0;
            while(true){
                try {
                    queue1.put(count);
                    System.out.println("生产者生产元素:"+count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //消费者
        Thread customer =new Thread(()->{
            while(true){
                try {
                    int ret=queue1.take();
                    System.out.println("消费者消费元素:"+ret);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        customer.start();
    }
}

Guess you like

Origin blog.csdn.net/m0_67995737/article/details/128891632