关于单例模式的一些思考
并发编程的有序性问题
**有序性:**顾名思义,有序性指的是程序按照代码的先后顺序执行。
导致代码不按照顺序执行的原因是指令重排。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。例如程序中:“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操作应该是:
- 第一步:分配一块内存,地址为M
- 第二步:在内存M上初始化
SingletonDemo
对象 - 第三步:将M的地址赋值给instance变量
但是实际上编译器优化(指令重排)后的执行路径却是这样的:
- 第一步:分配一块内存,地址为M
- 第二步:将M的地址赋值给instance变量
- 第三步:在内存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()的区别
- 声明位置不同:
- execute() 方法定义在 Executor 接口中,submit() 方法定义在 ExecutorService 接口中
- ExecutorService 接口继承了 Executor 接口
- 可传参数不同:
- execute() 方法参数只能传入 Runnable 接口
- submit() 方法参数可以传入 Runnable 接口 和 Callable接口
- 返回值不同:
- execute() 方法没有返回值
- submit() 方法返回 future 类型的对象,通过 future 类型的对象可以获取线程的返回值
- 异常处理不同:
- submit() 方法内部处理了线程运行期间抛出的异常,导致开发人员看不到线程是否发生了异常
- execute() 方法没有处理线程运行期间抛出的异常,线程发生异常会抛给出来
p2.start();
c1.start();
c2.start();
}