【JavaEE】多线程(4)

发布于:2024-12-06 ⋅ 阅读:(35) ⋅ 点赞:(0)

一、单例模式

1.1 设计模式

单例模式是一种经典的设计模式,什么是设计模式?

设计模式就是为各种经典的问题场景提供一些解决方案,遇到这个场景,代码就按照"前人"总结 的模式去写,代码就不会写到很差

其实设计模式和棋谱类似,一般在开局时,对手下出一步棋,你只要走出对应的谱招,你的局面肯定不会差

1.2 单例模式概念

单例模式中的单例就是单个实例,强制进程中的某个类有且只有一个对象,这个对象就是"单例",我们可以通过一些编码的技巧,使编译器可以识别类是否有多个对象,一旦尝试创建多个对象,就会编译报错

单例模式的实现有很多方式,本篇文章着重讲解"饿汉模式""懒汉模式"

1.3 饿汉模式

1.3.1 引入概念

饿汉模式在类加载时就创建实例

class Singleton {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {}
}

public class Demo {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance(); //和s1获取到的对象是相同的
        System.out.println("s1 = "+ s1);
        System.out.println("s2 = "+ s2);
    }
}

代码中,将构造方法用private,这样在尝试new一个对象时就会编译报错

Singleton s3 = new Singleton(); //编译报错

1.3.2 线程安全

饿汉模式是线程安全的,因为实例是在类加载时就已经被创建了,它的创建时比主线程还要早,所以当在main中启动其他线程时实例早就被创建了,每个线程调用 getInstance() 都是读取同一个变量的值(多个线程读取同一个变量→线程安全)

1.4 懒汉模式

1.4.1 引入概念

懒汉模式是在第一次需要这个实例时才被创建(如果后续不需要也就不会创建了,节省开销)

class SingletonLazy {
    private static SingletonLazy instance = null; //一开始为null
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy(); //调用getInstance()时才被创建
        }
        return instance;
    }

    private SingletonLazy() {}

}

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

    }
}

当代码中有多个单例类,如果使用饿汉模式,就会在程序创建时扎堆的创建实例,可能会拖慢程序的启动时间,使用懒汉模式,创建实例的时机是分散的,用户不太容易感知到卡顿

1.4.2 线程不安全

懒汉模式线程不安全,我们从时间轴的角度分析懒汉模式创建实例时的场景:

t2切换回来后,就会直接进入条件内部再次执行new操作,如此以来,就进行了两次new操作

1.4.3 解决线程不安全

解决上述线程不安全很简单——加锁,只要将if和new打包成一个原子操作就行了

public static SingletonLazy getInstance() {
    synchronized (locker) {
        if (instance == null) {
            instance = new SingletonLazy();
        }
    }
    return instance;
}

这样t1先获取到锁后创建完实例,再释放锁,此时t2获取到锁发现instance已经被创建了,就不会进行二次创建

在懒汉模式中,在开始创建实例时会发生线程不安全,后续其他线程再 getInstance 时都只是读取instance,线程是安全的,但此时加锁操作还是存在的,这样会使线程阻塞,开销加大,所以可以再加一层判断条件来判断是否要加锁

public static SingletonLazy getInstance() {
    if (instance == null) {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
    }    

    return instance;
}

如果instance不为空,证明实例已经被创建了,线程都是对这个变量进行读取操作,线程是安全的,所以没必要加锁

除此之外可以对instance加上volatile关键字,防止内存可见性问题,因为代码中没有大量的重复操作,大概率不会触发优化,所以内存可见性问题发生机率很小,加上为了保险

二、阻塞队列

2.1 什么是阻塞队列

阻塞队列是一种特殊的队列,遵守"先进先出"的原则

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

  • 当队列满的时候,继续入队列就会阻塞,直到其他线程从队列中取走元素
  • 当队列空的时候,继续出队列也会阻塞,直到其他线程往队列中插入元素

阻塞队列的一个典型应用场景就是"生产者消费者模型"(一个典型的开发模型)

2.2 生产者消费者模型

生产者消费者模型就是通过一个容器解决生产者和消费者的强耦合问题

站在A的角度,不知道B的存在,只关心和队列的交互;站在B的角度,不知道A的存在,只关心和队列交互,此时A如果挂了,也不会影响B(解耦合)

如果没有阻塞队列,让A直接调用B,意味着A的代码中就要包含很多和B相关的逻辑,B中的代码也会包含A相关的逻辑,彼此之间就有了耦合,A出现了bug就会牵连到B

生产者消费者模型的优势:

  • 如上述所言,可以使生产者和消费者之间解耦
  • 阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力(下面进行讲解)

如下图:

在没有阻塞队列的情况下,A收到一个客户端的请求,就要请求一次B,当A收到的请求激增,B收到的请求也会激增,假如A的工作比较轻量,消耗的资源少;B的工作比较重量,消耗的资源多,当请求激增时B就有可能挂,所以引入阻塞队列:

这样当A收到的请求激增时,只是像队列写入的速度加快,而B可以按照自己原有的节奏从队列中取,阻塞队列起到一个缓冲的作用,这样就不会导致挂掉

2.3 Java库中的阻塞队列

2.3.1 BlockingQueue接口

在Java标准库中内置了阻塞队列:

BlockingQueue是一个接口,继承自Queue,它的实现类有如下:

出队和入队方法如下:

void put(E e) throws InterruptedException; 入队,当队列已满时阻塞
E take() throws InterruptedException; 出队,当队列为空时阻塞

在阻塞过程,如果其他线程使用interrupt方法,put 或 take 就会抛出异常,终止阻塞

2.3.2 方法演示

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

        queue.put("abc");
        String elem = queue.take();
        System.out.println(elem); // 打印 abc
        queue.take(); // 线程进入阻塞状态
    }

2.3.3实现生产者消费者模型

下面使用阻塞队列实现生产者消费者模型

class Demo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费元素:"+ value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "消费者");

        Thread producer = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    int num = count++;
                    System.out.println("生产元素:"+ num);
                    queue.put(num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "生产者");

        customer.start();
        producer.start();

    }
}

生产者每隔1s生产一个元素,由于阻塞队列的存在,消费者只能每隔1s从队列中消费一个元素

2.3.4 手动实现阻塞队列

自己实现的阻塞队列可以使用泛型,这里就直接假定队列中的元素都是String类型

class BlockingQueue {
    String[] elems;

    public BlockingQueue (int capacity) {
        elems = new String[capacity];
    }
  
    public int head = 0; // 第一个元素所在的位置

    public int tail = 0; // 最后一个元素所在位置的下一个位置

    public int size = 0; // 队列中的元素个数

    //向队列中添加元素
    public void put (String elem) throws InterruptedException {
        synchronized (this) {
            while (size >= elems.length) {
                //阻塞
                this.wait();
            }
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }

            size++;
            this.notify();
        }
    }

    //从队列中取元素
    public String take () throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //阻塞
                this.wait();
            }
            String result = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            
            size--;
            this.notify();
            return result;
        }
    }
}

这里运用了循环队列来实现阻塞队列,head、tail、size如上述注释

在put和take方法中有大量的对变量修改的操作,为了保证线程安全,统一加上锁,锁对象就是当前对象

在put方法中,当队列中的元素已满时就阻塞,进入阻塞使用的 while 而不是 if ,因为当wait 被唤醒后会再判断一次条件,wait方法不只会被notify唤醒,当其他线程interrupt了一下,也会使wait返回,代码可能会继续执行,继续往队列中添加元素,这样就会覆盖掉已经存在的元素


🙉本篇文章到此结束