week6总结

本文探讨了单例模式在并发编程中的挑战,特别是如何通过volatile和synchronized关键字保证线程安全,以及指令重排对代码有序性的影响。通过实例展示了双检锁的潜在问题,并深入解析了synchronized在不同场景下的同步机制。
摘要由CSDN通过智能技术生成

关于单例模式的一些思考

并发编程的有序性问题

**有序性:**顾名思义,有序性指的是程序按照代码的先后顺序执行。

导致代码不按照顺序执行的原因是指令重排。

​ 编译器为了优化性能,有时候会改变程序中语句的先后顺序。例如程序中:“a=6;b=7;” 编译器优化后可能变成 “b=7;a=6;”。在这个例子中,编译器调整了语句的顺序,这种调整顺序称为指令重排,指令重排不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。

Java 实现有序性的办法:

  • volatile 关键字可以禁止指令进行重排序优化,从而保证有序性
  • synchronized关键字可以保证共享变量的有序性
  • lock可以防止指令重排

示例:并发编程中的有序性问题

/**
 * 单例程序
 */
public class SingletonDemo {
    //定义一个instance,不new
    private  static SingletonDemo INSTANCE;

    //构造方法设为私有的,不让别人new
    private SingletonDemo() {
    }

    //提供一个getInstance方法
    public static SingletonDemo getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);//为了测试效果,休眠1毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new SingletonDemo();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        //100个线程创建单例对象,应该创建出来一个对象
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonDemo.getInstance().hashCode())).start();
        }
    }
}

分析代码:

​ 假设线程A在获取实例getInstance()的方法中,首先判断instance是否为空,如果为空则创建SingletonDemo的一个实例。但是在线程A判断和创建线程之间的这段时间里,来了线程B,线程B在获取实例getInstance()的方法中,判断instance也为空,结果导致线程B也创建了SingletonDemo的一个实例。运行该程序,观察运行结果,发现创建对象的hashCode值不同,说明不是一个对象,创建单例失败。

运行结果

​ 1343599390
​ 1926683415
​ 364927716
​ 112216044
​ 795884320

在Java领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:

/**
 * 双重验证单例程序
 */
public class SingletonDemo {
    //定义一个instance,不new, 不使用volatile修饰变量instance
    private static SingletonDemo instance ;

    //构造方法设为私有的,不让别人new
    private SingletonDemo() {
    }

    //提供一个getInstance方法
    public static SingletonDemo getInstance() {
        //第一重验证
        if (instance  == null) {
            //对当前类加锁
            synchronized (SingletonDemo.class) {
                //第二重验证
                if (instance  == null) {
                    instance  = new SingletonDemo();
                }
            }
        }
        return instance ;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonDemo.getInstance().hashCode())
            ).start();
        }
    }
}
  • 在获取实例getInstance()的方法中,我们首先判断instance是否为空(第一重验证),如果为空,则锁定SingletonDemo.class并再次检查instance是否为空(第二重验证),如果还为空则创建Singleton的一个实例。
  • 假设有两个线程A、B同时调用getInstance()方法,他们会同时发现 instance == null(第一重验证) ,于是同时对SingletonDemo.class加锁,此时JVM保证只有一个线程能够加锁成功(假设是线程A),另外一个线程则会处于等待状态(假设是线程B)
  • 线程A会创建一个SingletonDemo实例,之后释放锁,锁释放后,线程B被唤醒,线程B再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程B检查 instance == null 时会发现,已经创建过Singleton实例了,所以线程B不会再创建一个Singleton实例。

​ 这看上去一切都很完美,无懈可击,但实际上这个getInstance()方法并不完美。问题出在哪里呢?出在new操作上,我们以为的new操作应该是:

  1. 第一步:分配一块内存,地址为M
  2. 第二步:在内存M上初始化SingletonDemo对象
  3. 第三步:将M的地址赋值给instance变量

但是实际上编译器优化(指令重排)后的执行路径却是这样的:

  1. 第一步:分配一块内存,地址为M
  2. 第二步:将M的地址赋值给instance变量
  3. 第三步:在内存M上初始化SingletonDemo对象

第二步和第三步交换了顺序。

​ 优化后会导致什么问题呢?我们假设线程A先执行getInstance()方法,当执行完第二步时恰好发生了线程切换,切换到了线程B上;如果此时线程B也执行getInstance()方法,那么线程B在执行第一个判断时会发现 instance != null,所以直接返回instance,而此时的instance是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
在这里插入图片描述

解决方法:用 volatile 修饰 instance。

volatile 修饰的变量在赋值时禁止指令重排。

private static SingletonDemo instance ;

修改为

private volatile static SingletonDemo instance ;//添加了 volatile 关键字,能防止指令重排

关于synchronized的一些思考

synchronized 同步代码块

  • 锁相同,可以实现同步
//锁相同,可以实现同步
public class Test01 {

    public static void main(String[] args) {
        Test01 obj = new Test01();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm();//使用的锁对象this就是obj对象
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm();//使用的锁对象this也是obj对象
            }
        }).start();
    }

    // 定义方法,打印 100 行字符串
    public void mm() {
        //这是临界区
        synchronized (this) {//经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + " --> " + i);
            }
        }
    }
}
  • 锁不同,不能实现同步
//锁不同,不能实现同步
public class Test02 {
    public static void main(String[] args) {
        Test02 obj = new Test02();
        Test02 obj2 = new Test02();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm();//使用的锁对象this就是obj对象
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj2.mm();//使用的锁对象 this 也是 obj2对象
            }
        }).start();
    }

    public void mm() {
        //这是临界区
        synchronized (this) {//经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + " --> " + i);
            }
        }
    }
}
  • 使用一个常量对象作为锁对象(可以实现同步)
// 使用一个常量对象作为锁对象(可以实现同步)
public class Test03 {
    public static void main(String[] args) {
        Test03 obj = new Test03();
        Test03 obj2 = new Test03();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm();//使用的锁对象 OBJ 常量
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj2.mm();//使用的锁对象 OBJ 常量
            }
        }).start();
    }

    public static final Object OBJ = new Object();

    //定义一个常量,
    public static void mm() {
        synchronized (OBJ) {//使用一个常量对象作为锁对象
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + " --> " + i);
            }
        }
    }
}

同步方法

  • 实例方法的锁是this
//把整个方法体作为同步代码块,默认的锁对象是 this 对象
public class Test05 {
    public static void main(String[] args) {
        Test05 obj = new Test05();   //obj是一把锁
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.mm2();            //使用的锁对象this就是obj对象
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test05 obj1 = new Test05(); //新new的对象,产生了一把新的锁obj1
                obj1.mm2();           //obj1 和 obj不是同一个锁,不能实现同步 
                obj.mm2();            //使用的锁对象 this 也是 obj对象, 可以同步
            }
        }).start();
    }

    //使用 synchronized 修饰实例方法,同步实例方法, 默认 this 作为锁对象
    public synchronized void mm2() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + " --> " + i);
        }
    }
}
  • 静态方法的锁是类名.class
//把整个静态方法体作为同步代码块,默认的锁对象是当前类的运行时类对象, Test06.class, 有人称它为类锁
public class Test06 {
    public static void main(String[] args) {
        Test06 obj = new Test06();
        new Thread(new Runnable() {
            public void run() {
                obj.mm2();//使用的锁对象是 Test06.class
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test06.mm2();//使用的锁对象是 Test06.class
            }
        }).start();
    }

    //使用 synchronized 修饰静态方法,同步静态方法, 默认运行时类作为锁对象
    public synchronized static void mm2() {//synchronized(Test06.class)
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + " --> " + i);
        }
    }
}

生产者消费者模式

在 Java 中,负责产生数据的模块是生产者,负责使用数据的模块是消费者。生产者消费者解决数据的平衡问题,即先有数据然后才能使用,没有数据时,消费者需要等待。

【单个生产者,单个消费者】

(1)定义能存放数据、能生产数据、能消费数据的类

//定义一个操作数据的类
class Data {
    //value是被操作的数据
    private String value = "";

    //生产数据的方法(就是给value赋值)
    public void setValue() {
        synchronized (this) {
            //单个生产者用if
            if (!value.equalsIgnoreCase("")) {
                try {
                    this.wait();//如果数据还没有被消费,就等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String value = System.currentTimeMillis() + " - " + System.nanoTime();
            System.out.println("set 设置的值是: " + value);
            this.value = value;//生产者生产数据
            this.notifyAll();//生产者唤醒消费者
        }
    }

    //消费数据的方法(就是从value取值)
    public void getValue() {
        synchronized (this) {
            //单个消费者用if
            if (value.equalsIgnoreCase("")) {
                try {
                    this.wait();//如果数据还没有被生产,就等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("get 的值是: " + this.value);
            this.value = "";//消费者消费数据
            this.notifyAll();//消费者唤醒生产者
        }
    }
}

(2)定义生产者线程类

/**
 * 定义线程类模拟生产者
 */
class ProducerThread extends Thread {
    //定义存放数据的对象data
    private Data data;

    public ProducerThread(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true) {
            //生产者生产数据就是调用 Data 类的 setValue 方法给 value 字段赋值
            data.setValue();
        }
    }
}

(3)定义消费者线程类

// 定义线程类模拟消费者
class ConsumerThread extends Thread {
    //定义存放数据的对象data
    private Data data;

    public ConsumerThread(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true) {
            //消费者消费数据就是调用 Data 类的 getValue 方法获取 value 字段的值
            data.getValue();
        }
    }
}

定义测试类

//测试生产,消费的情况
public class Thread08 {
    public static void main(String[] args) {
        Data data = new Data();
        ProducerThread p = new ProducerThread(data);
        ConsumerThread c = new ConsumerThread(data);
        p.start();
        c.start();
    }
}

【多个生产者,多个消费者】

将生产数据的setValue()方法中的if修改为while就满足多个生产者

public void setValue() {
    synchronized (this) {
        //将单个生产者的if修改为while
        while (!value.equalsIgnoreCase("")) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String value = System.currentTimeMillis() + " - " + System.nanoTime();
        System.out.println("set 设置的值是: " + value);
        this.value = value;
        this.notifyAll();
    }
}

将消费数据的 getValue() 方法中的if修改为while就满足多个消费者

public void getValue() {
    synchronized (this) {
        //将单个消费者的if修改为 while
        while (value.equalsIgnoreCase("")) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("get 的值是: " + this.value);
        this.value = "";
        this.notifyAll();
    }
}

在测试类中创建多个生产者和消费者

public static void main(String[] args) {
    Data data = new Data();
    ProducerThread p1 = new ProducerThread(data);
    ProducerThread p2 = new ProducerThread(data);
    ConsumerThread c1 = new ConsumerThread(data);
    ConsumerThread c2 = new ConsumerThread(data);
    p1.start();
    p2.start();
    c1.start();
    c2.start();
}

execute() 和 submit()的区别

  1. 声明位置不同:
    1. execute() 方法定义在 Executor 接口中,submit() 方法定义在 ExecutorService 接口中
    2. ExecutorService 接口继承了 Executor 接口
  2. 可传参数不同:
    1. execute() 方法参数只能传入 Runnable 接口
    2. submit() 方法参数可以传入 Runnable 接口 和 Callable接口
  3. 返回值不同:
    1. execute() 方法没有返回值
    2. submit() 方法返回 future 类型的对象,通过 future 类型的对象可以获取线程的返回值
  4. 异常处理不同:
    1. submit() 方法内部处理了线程运行期间抛出的异常,导致开发人员看不到线程是否发生了异常
    2. execute() 方法没有处理线程运行期间抛出的异常,线程发生异常会抛给出来
      p2.start();
      c1.start();
      c2.start();
      }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值