1. 懒汉式
概念:使用的时候才初始化,jvm只有一个实例。
public class LazyMan {
public LazyMan() {
System.out.println(Thread.currentThread().getName()+"ok");
}
public static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 300; i++) {
new Thread(() -> getInstance()).start();
}
}
}
会出现:
可见并没有实现单例模式。
1.1实现方式1
如果在多线程下使用:<mark>必须加上synchronized才可以保证只有一个对象被创建</mark>
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(()->{
LazySingleton lazySingleton1 = LazySingleton.getInstance();
System.out.println(lazySingleton1);
}).start();
new Thread(()->{
LazySingleton lazySingleton2 = LazySingleton.getInstance();
System.out.println(lazySingleton2);
}).start();
}
}
class LazySingleton{
private static LazySingleton lazySingleton = null;
//构造器私有
private LazySingleton(){
}
//获取实例
public static synchronized LazySingleton getInstance(){
if( lazySingleton == null ){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
优化LazySingleton类,如果我们将synchronized加在了静态方法上,那么锁住的就是整个类,我们可以这样优化:
//获取实例(优化)
public static LazySingleton getInstance(){
if( lazySingleton == null ){
synchronized (LazySingleton.class){
if( lazySingleton == null )
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
分析:
如果线程1先进入同步代码块后,线程2在外面等候,当线程1完成对象创建后,此时lazySingleton对象已经不为空,就不会再创建lazySingleton对象了,实现了单例设计,优化了锁的范围,提高了效率。
优化引用变量
在我们进行多线程编程时,会出现new出来的变量为空,导致xxx线程的对象是空的,引发空指针异常,为什么会出现这个情况呢?出现这个问题很重要的原因是JVM虚拟机会对一些指令进行重排序,例如:
正常的顺序:
//我们new 一个对象
Demo demo = new Demo();
//可以使用javap命令对class字节码文件进行反汇编,创建Demo对象具体指令
0:new #2 1.分配空间---->返回一个指向该空间的内存引用
3:dup 2.堆栈的初始化
4:invokespecial #3 3.对分配的空间进行初始化
7:astore_1 4.把内存引用赋值给demo变量
重排序(就是顺序可能会乱掉):
1.分配空间---->返回一个指向该空间的内存引用
2.堆栈的初始化
4.把内存引用赋值给demo变量
3.对空间进行初始化
为了避免这一情况的出现,可以使用volatile关键字修饰引用变量
private static volatile LazySingleton lazySingleton = null;
作用就是可以防止JVM指令重排。
<mark>使用注意点</mark>
- 需要保证线程安全(synchronized)
- 双重检查优化synchronized
- 防止指令重排
1.2 实现方式2
- 使用静态内部类实现懒加载
- 主要依据:静态内部类调用的时候才加载
class LazySingleton2{
//构造器私有
private LazySingleton2(){
}
//静态内部类
private static class InnerClass{
private static LazySingleton2 lazySingleton2 = new LazySingleton2();
}
//获取对象
public static LazySingleton2 getInstance(){
return InnerClass.lazySingleton2;
}
}
但是,需要注意反射攻击,所谓反射攻击,就是在new完一个对象后,还可以通过反射来再创建一个对象,这在单例模式里是不被允许的。
演示反射攻击:
/** * 通过反射攻击来影响LazySingletonTest2类的单例模式 */
public class ReflectAct {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<LazySingleton2> constructor = LazySingleton2.class.getDeclaredConstructor();
constructor.setAccessible(true); //设置访问权限
LazySingleton2 lazySingleton2 = constructor.newInstance();
LazySingleton2 instance = LazySingleton2.getInstance();
System.out.println(lazySingleton2 == instance); //false
}
}
可见,反射确实可以干预单例模式,解决方法:<mark>修改私有构造</mark>
//获取对象
public static LazySingleton2 getInstance(){
if(InnerClass.lazySingleton2 != null) {
throw new RuntimeException("单例模式不允许创建多个实例...");
}
return InnerClass.lazySingleton2;
}
补充:在枚举类型的Class类中已经进行了处理,所以枚举类型可以防止反射工具。
2. 饿汉式
概念:饿汉式加载当类一加载完就创建对象
类加载步骤:
- 加载对应的二进制文件进方法区,创建对应的数据结构
- 连接:a.验证 b.准备(默认值的赋值) c.解析
- 初始化:给静态变量赋值
public class HungrySingletonTest {
public static void main(String[] args) {
}
}
class HungrySingleton{
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}