上文我们了解了如何创建多线程,以及在多线程下由于不同的操作,可能引发很多线程不安全问题,今天我们就线程使用的案列吧
1. 多线程案例
1.1 单例模式
单例模式能保证某个类在程序中只存在唯⼀⼀份实例,而不会创建出多个实例.
那么我们该如何一个程序中对象是单例的,用什么方法去保证??
- 人为口头约定,大家不要new 这个对象,我给大家提供一个方法,这个方法可以返回一个单例的对象,但是毕竟是人为操作,很容易出错
- 通过语言自身的语法约束,限制一个类只能被实例化一个对象,把限制的过程交给程序,程序写死了就按照写的逻辑执行,不会改变,只要代码层面能保证是单例,那么执行后一定是个单例,不会出现问题
所以我们采用方式2,在Java中单例模式具体的实现方式有很多.最常⻅的是"饿汉"
和"懒汉"
两种.
1.1.1 饿汉模式
类加载的时候就完成对象初始化的创建方式称为’饿汉模式’
实现过程:
1. 要实现单例类, 只需要定义一个static修饰的变量,就可以保证这个变量全局唯一(单例)
说明:
- private修饰是为了防止外部对这个变量赋值(修改)
- static是为了保证全局唯一
- new SingletonHungry(),表示当类加载到JVM中的时候,就会实列这个变量
2. 既然是单例,就不想让外部去new 这个对象,此时我们就要将SingletonHungry当前类的构造方法私有化
此时从语法上就不能new对象了
3. 把获取对象的方法改为static,通过类名.方法名的方式调用
完整饿汉模式创建如下:
/**
* 饿汉模式
*/
public class SingletonHungry {
//定义一个类的成员变量,用static修饰,保证全局唯一
private static SingletonHungry instance=new SingletonHungry();
//由于是单例就不想外部就new,所以将构造方法私有化
private SingletonHungry(){
}
//把获取对象的方法改为static,通过类名.方法名的方式调用
public static SingletonHungry getInstance() {
return instance;
}
}
最后通过一段测试代码,看我们的饿汉模式是否成功了,若打印的对象地址相同则成功,否则失败
public class SingletonHungryTest {
public static void main(String[] args) {
SingletonHungry instance1=SingletonHungry.getInstance();
SingletonHungry instance2=SingletonHungry.getInstance();
SingletonHungry instance3=SingletonHungry.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
}
}
我们发现成功了!!
总结:
由于程序在启动的时候可能需要加载很多的类(单例类),并不一定要在程序启动的时候用
为了节省计算机资源,加速程序的启动,可以让单例类在用到的时候再进行初始化(new),延迟初始化
在编程中延时加载是个褒义词
1.1.2 懒汉模式-单线程版
类加载的时候不创建实例.第⼀次使用的时候才创建实例.
实现过程:
- 只声明这个全局变量,不初始化
- 在获取单例对象的时候加一个是否为的判断,空则创建对象
- 不想外部new新对象
完整懒汉模式实现代码如下:
public class SingletonLazy {
//在未使用前不实例化对象
private static SingletonLazy instance=null;
//由于是单例就不想外部就new,所以将构造方法私有化
private SingletonLazy(){
}
//在获取单例对象的时候加一个是否为的判断,空则创建对象
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
实现完成后,我们测试在单线程和多线程下是否线程安全??
- 单线程
public class SingletonHungryTest {
public static void main(String[] args) {
SingletonHungry instance1=SingletonHungry.getInstance();
SingletonHungry instance2=SingletonHungry.getInstance();
SingletonHungry instance3=SingletonHungry.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
}
}
- 多线程
public class SingletonLazyTestMulti {
public static void main(String[] args) {
//创建10个线程
for (int i = 0; i < 10; i++) {
Thread thread=new Thread(()->{
SingletonLazy instance=SingletonLazy.getInstance();
System.out.println(instance);
});
thread.start();
}
}
}
究竟是何种原因导致的呢,我们见下文细说
1.1.3 懒汉模式-多线程版
1. 为何懒汉模式下的多线程有线程安全问题呢?
由于instance是一个共享变量
创建对象并赋值,是一个修改操作
多个线程对共享变量进行修改导致引发了线程安全问题
小tip: 只要变量在运算符左边,都可以理解为先LOAD再进行操作
2. 那么我们该如何解决当前线程安全问题呢??
前面我们对线程安全问题有两种关键字可以帮助我们解决,synchronize和volatile,由于此处是由于原子性问题导致的,我们可以采用synchronize关键字将初始化有关的代码块加锁即可
懒汉模式-多线程版代码块如下:
public class SingletonLazy {
//在未使用前不实例化对象
private static SingletonLazy instance=null;
//由于是单例就不想外部就new,所以将构造方法私有化
private SingletonLazy(){
}
//在获取单例对象的时候加一个是否为的判断,空则创建对象
public static SingletonLazy getInstance() {
//对初始化相关的代码块加锁,可以解决问题
synchronized (SingletonLazy.class){
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
}
1.1.4 懒汉模式-多线程版(改进)
1.上述代码块的写法还有一个问题需要处理?
我们思考一个问题:
- 当第一个线程进行这个方法时,如果变量没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成
- 后面的线程以后也永远不会再执行new 对象的操作
- synchronized还有没有必要加了?
由于synchronized关键字对应了CPU上的指令,LOCK和UNLOCK对应的锁指令是互斥锁,比较消耗系统资源,从第二个线程开始这个加锁解锁都是无效的操作,所以我们可以考虑是否减小加锁的次数,我们只需要在加锁前判断是否需要加锁即可
2.在DCL当中是否要加volatile??
我们来分析一下: 在Java中new一个对象的步骤如下:
1.在内存中申请一片空间
2.初始化对象的属性(赋初值)
3.把对象在内存中的首地址赋给对象的引用 其中1.3 是强相关的关系,2并不强相关就有可能发生指令重排序
正常:123 重排序:132 为变量加volatile修饰,禁止指令重排序,初始化指令的执行顺序为123
为了避免"内存可⻅性"导致读取的instance出现偏差,于是补充上volatile.
建议: 只要在多线程环境中修改了共享变量就要给共享变量加volatile
懒汉模式改进版代码如下:
/**
* Double Check Lock 双重检查锁
*/
public class SingletonLazyDCL {
private static volatile SingletonLazyDCL instance=null;
private SingletonLazyDCL(){}
public static SingletonLazyDCL getInstance(){
if(instance==null){
synchronized (SingletonLazyDCL.class){
if(instance==null){
instance=new SingletonLazyDCL();
}
}
}
return instance;
}
}
多线程案例之阻塞队列请见下文!