Java EE——线程安全和单例模式

wait和sleep对比

这两个关键字都是使线程进入阻塞等待的状态。区别就是sleep使时间到了就自己唤醒了,而wait是需要别的线程调用notify才能唤醒。
wait还有一种重载版本,可以传时间参数,代表最大的等待时间

wait 和 notify demo

我们可以通过wait和notify来管理线程执行的顺序,进而影响输出。下面这个demo就是通过三个线程互相调用wait和notify,相互唤醒,以达到循环输出10次abc的目的

public class homework2 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    private static Object locker3 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker1){
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A");
                    synchronized (locker2){
                        locker2.notify();
                    }
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker2){
                    try {
                        locker2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("B");
                    synchronized (locker3){
                        locker3.notify();
                    }
                }
            }
        });
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker3){
                    try {
                        locker3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("C");
                    synchronized (locker1){
                        locker1.notify();
                    }
                }
            }
        });

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

        synchronized (locker1){
            locker1.notify();
        }
    }
}

单例模式

单例模式是一种常见的设计模式。

由于并不是所有的程序员都能够保证自己的代码不会出现一些常见的错误,因此有些优秀的程序员设计了一些设计模式,通过按照设计模式编写代码,可以避免一些错误。

所谓单例模式,就是限制一个类值只能有一个实例。例如之前的jdbc代码中的DataSource,我们只需要一个实例。

我们的单例模式就是为了避免一时马虎,从而犯下的bug。其原理就是通过java本身的语法特性,限制了我们的代码写法

而java中我们的static修饰符就是可以让我们只拥有一个实例,事实上,其本质相当于是类对象的属性,而类对象是通过jvm加在.class文件得到的,而.class文件只能加载一次,因此就只有一个

static关键字介绍

我们的static关键字在c语言中代表将变量放到静态内存中,随后由于静态变量的消失,static演变成下面几个用途

  1. 修饰局部变量,改变了变量的生命周期,让静态局部变量出了作用域依然存在,程序结束,生命周期才结束。
  2. 修饰全局变量,改变了变量的作用域,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
  3. 修饰函数,改变了其作用域,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

而在c++中,static用来控制变量的存储方式和可见性

实现单例模式

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

我们在代码类创建过程中就把对象创建出来,这样其他人就创建不了了,但是还有一个问题,别人可以通过构造方法来创建新的实例
因此我们把构造方法也变成private就可以了

饿汉模式

public class SingletonDemo {
    private static SingletonDemo instance = new SingletonDemo();

    public static SingletonDemo getInstance(){
        return instance;
    }

    private SingletonDemo(){
        
    }

    public static void main(String[] args) {
        SingletonDemo instance = SingletonDemo.getInstance();
    }
}

事实上,上面我们的单例模式是饿汉模式,也就是类加载阶段就把实例创造好了,相对应的还有懒汉模式,也就是需要创建的时候再创建,这样的话可以提升效率,节省资源

懒汉模式

public class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){
        
    }
}

当我们调用getInstance方法时,如果已经有实例了,就返回实例,否则就创建实例

线程安全问题

通过代码分析,我们可以发现饿汉模式是线程安全的,而懒汉模式是线程不安全的。
因为懒汉模式中的if相当于“读”操作,而new相当于“写”操作,如果一个线程刚判断这个instance是null的,还没来得及创建实例,另一个线程这个时候也读取了instance的值,然后也创建了一个实例,这个时候我们就有两个实例了,就不安全了

我们可以用synchronized,让我们代码变得满足原子性,从而使懒汉模式的单例模式线程安全

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

    }
}

我们的synchronized中的对象就传入SingletonLazy的类对象

然而,虽然解决了线程不安全问题,但是我们的代码在单线程情况下就会变得效率低下,这是因为synchronized这个锁本身调用也是有开销的,这也是为什么vector虽然线程安全,但是并不推荐使用,就是因为里面synchronized关键字的滥用导致效率低下

因此我们希望在实例没有创建之前,不加synchronized,在实例创建之后,再加synchronized

解决效率问题

public class SingletonLazy {
    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(){

    }
}

事实上,我们的代码还有一个问题
当我们两个线程同时想要获取实例时,如果第一个线程进入synchronized开始new对象
由于new对象在cpu上存在多个指令

  1. 申请内存,得到首地址
  2. 调用构造方法,初始化实例
  3. 将内存首地址赋予实例引用
    如果我们的cpu为了提升效率,进行了指令重排序,将2和3调换了顺序,这时如果在我们的线程1刚执行完3,还没执行到2时,那么线程1中的instance虽然有内存的地址,但是地址上并没有相应的数据
    这时线程2调用了这个getInstance方法,并且对这个instance进行了解引用操作,那么就会出现问题

解决指令重排序问题

事实上,解决指令重排序问题和解决内存可见性问题的关键字都是volatile
因此我们这要把instance这个变量用volatile修饰就好了

public 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(){

    }
}

你可能感兴趣的:(java,单例模式,java-ee,java)