目录
- 单例模式
- 1、饿汉式
- 2、懒汉式
- 3、DSL懒汉式(双重锁懒汉模式)
- 静态内部类懒汉式
- 单例模式的如何破坏
- 4、使用枚举类
单例模式
为什么使用单例模式? 单例模式确保一个类在内存中只会有同一个实例对象存在。不管声明获取实例对象多少次都是内存中同一块地址。
不管哪一种单例模式都是要将自己的构造器进行私有化,防止外面通过构造器创建出多个不同的实例化对象。对象是在对应类的内部通过调用自身的构造方法创建的,然后以static方法的方式向外提供。
1、饿汉式
在类启动时就会将创建实例对象放在内存中。
饿汉式的思路就是1.将构造器私有化 2.声明一个私有的静态的
类类型的变量
并在类的内部通过构造函数为其赋值 3. 通过静态方法将其暴露出去
在使用时,只需要的调用该类向外提供的静态方法即可
但这种饿汉式的单例模式有一个非常大的缺点就是,如果一个类中方法变量十分的多,且在不能确定是否需要该实例的情况下,直接将对象加载到内存会十分耗费内存。
2、懒汉式
只有用到时此对象时,才会创建类实例对象将其放入内存中,这样就可以防止将不必要的对象加载到内存,节省资源。相较于饿汉式是一种以时间换空间
的方式。
懒汉式思路:1. 将构造器私有化 2. 声明一个私有的静态的
类类型的变量
,注意没有赋值 3. 在向外提供实例对象时,进行判断是否已经创建对象,如果没有再为其创建对象。统一向外暴露
懒汉式相较于饿汉式是在需要此对象时(调用此类向外提供暴露对象的方法)才会创建对象,再创建对象时还会判断是否已经存在此类对象,如果存在则继续使用,不存在作为new一个新的对象,确保内存中只有一个此类的对象。
但这种懒汉式也存在一个问题,就是线程不安全。当在多线程环境下使用获取此对象时,由于判断与操作不是一体可能导致线程安全问题
线程安全问题产生原因:只有在第一次获取且多线程时才有可能出现线程安全问题
这样就不会产生线程安全性问题:
具体的解决方式,要使用DSL懒汉式或静态内部类懒汉式解决
3、DSL懒汉式(双重锁懒汉模式)
DSL:Double Check Lock
思考:如果直接在获取对象的静态方法上加上synchronized
会有什么影响?使得获取对象的操作变为了同步操作。无论是第一次创建对象还是已有对象的返回都是同步操作。在上面已知只有在第一次获取对象时在多线程的环境下才有可能发生线程安全问题。全部锁上,一律变成单线程操作会影响性能。
思考:为什么在变量User前使用volatile?
volatitle的三大特点:
- 可见性
- 不保证原子性
- 禁止指令重排
在User变量前加上了Volatile关键字,这里就用到了它的禁止指令重排
的特性。那么什么是指令重排呢?要知道,虽然在我们自己的代码中只有一行代码user =new User()
,但在JVM内部他可能分为三步执行:1. 在堆内存开辟空间 2. 在开辟的空间保存对象信息 3. 由栈内对象变量指向堆内存的空间 。但是,JVM为了效率存在乱序执行的可能,原本是1->2->3,乱序后就可能发生 1->3->2,虽然最后的结果是一样,但放在代码中,当有多个线程去执行它的时候,就会有小概率发生异常
而在User前使用Volatile关键字修饰,就可以防止user变量赋值指令发生重排,能够按照1、2、3的顺序执行不乱序。确保返回的对象变量在堆空间已经完成赋值
思考:为什么在类中锁住的是user.class? synchronized 锁类与锁对象的区别?
-
synchronized加在非静态方法锁住的是对象,加载静态方法锁住的是整个class类。例如:我在一个
F
非静态方法前上synchronized关键字,那么它的作用范围只是对于同一个对象来说的。同一个对象,即使在多个线程中调用此方法,在同一时刻内只能有一个线程进入了此方法。但如果我创建了两个对象A,B,分别在三个线程中(线程T1,T2使用对象A,线程T3使用对象B)调用F
这一相同方法,那么会发生什么呢?
同一时刻,在F方法中,线程(T1或T2)可能与T3同时调用这一个方法,而同一个时刻,线程T1,T2在只能有一个线程在执行F方法。
这就是对象锁,同一个时刻,在多线程中同一个对象只能有一个线程执行此方法
-
synchronized 加在静态方法锁住的类,这个类创建的所有对象在多线程中只能有一个对象在执行此方法
还是上面的例子,如果方法使用的是synchronized锁住类,那么同一个时刻,T1,T2,T3三个线程中只能有一个线程的一个对象在执行此方法
以下是针对synchronized的六种情况测试: (这里使用sleep是为了增大出现线程安全问题概率,并无实际意义。否则业务简单执行过快,根本没有给其他插入机会 )
- 测试
同一个
对象不加任何锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync = new TestSync();
new Thread(testSync::start,"A").start();
new Thread(testSync::start,"B").start();
new Thread(testSync::start,"C").start();
}
}
class TestSync{
void start(){
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
结果:
统一时刻,三个线程操作同一个对象进入方法
- 测试
不同
个对象不加任何锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync1 = new TestSync();
TestSync testSync2 = new TestSync();
TestSync testSync3 = new TestSync();
new Thread(testSync1::start,"A").start();
new Thread(testSync2::start,"B").start();
new Thread(testSync3::start,"C").start();
}
}
class TestSync{
void start(){
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
- 测试
同个
对象加对象锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync = new TestSync();
new Thread(testSync::start,"A").start();
new Thread(testSync::start,"B").start();
new Thread(testSync::start,"C").start();
}
}
class TestSync{
synchronized void start(){
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
思考:结果是什么?
因为锁住的是对象,而多个线程操作的是同一个对象,也就是说,只能同时有一个线程执行此方法
- 测试
不同个
对象加对象锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync1 = new TestSync();
TestSync testSync2 = new TestSync();
TestSync testSync3 = new TestSync();
new Thread(testSync1::start,"A").start();
new Thread(testSync2::start,"B").start();
new Thread(testSync3::start,"C").start();
}
}
class TestSync{
synchronized void start(){
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
思考:结果是什么?
因为加的是对象锁,但每个线程对象都不一样,所以多个线程都可以进入方法,对象锁无效
- 测试
同个
对象加类锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync = new TestSync();
new Thread(testSync::start,"A").start();
new Thread(testSync::start,"B").start();
new Thread(testSync::start,"C").start();
}
}
class TestSync{
void start(){
synchronized (TestSync.class){ // 在一个普通方法使用代码块的方式锁住一个类
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
思考:结果是什么?
锁住的是各个对象,同一个时刻只能有一个对象的线程去访问此方法,所以结果和对象锁相同
- 测试
不同个
对象加类锁
,在高并发环境下测试线程安全问题:
public class Application {
public static void main(String[] args) throws InterruptedException {
TestSync testSync1 = new TestSync();
TestSync testSync2 = new TestSync();
TestSync testSync3 = new TestSync();
new Thread(testSync1::start,"A").start();
new Thread(testSync2::start,"B").start();
new Thread(testSync3::start,"C").start();
}
}
class TestSync{
void start(){
synchronized (TestSync.class){ // 在一个普通方法使用代码块的方式锁住一个类
System.out.println(Thread.currentThread().getName() + "begin");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
思考:结果是什么?
即使是多个对象,但同一个时刻只能有一个对象的线程去访问此方法
静态内部类懒汉式
思路:1. 将构造私有化 2. 创建一个静态内部类 3. 在此内部类的内声明外部类对象类型的变量作为内部类属性,并对外部类对象变量使用外部类的私有构造为其赋值 4. 在外部类创建公有且静态方法,在方法中向外提供静态内部类声明的外部类对象
注意:静态内部类懒汉式也是线程安全的
思考:为什么静态内部类的方式单例模式是懒汉式?
因为程序启动时,JVM会将外部类加载到内存中进行初始化,而内部类不会加载,也就不会初始化。只有在使用时,才会将类进行初始化,为其静态变量进行赋值。也就是说,只有调用getInstance()
这个外部类方法时才会加载到内存, 这就是为什么是懒汉式。
思考:为什么静态内部类的方式是线程安全的呢?
静态内部类的静态属性是JVM在加载此类时进行赋值,而JVM在初始化此类时会为其上锁,使得在多线程环境下,内部类静态变量赋值时依然是线程安全的。
思考:静态内部类的方式与DSL的方式有什么区别?或者说静态内部类构造单例模式有什么缺点?
静态内部类最大的缺点就是:由于采用静态变量赋值,无法从外部传递参数
例:
public class Application {
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(User.getInstance(1).getAge());
},"A").start();
new Thread(()->{
System.out.println(User.getInstance(2).getAge());
},"B").start();
new Thread(()->{
System.out.println(User.getInstance(3).getAge());
},"C").start();
}
}
class User{
private static User user;
private int age;
private User(){
}
public int getAge(){
return this.age;
}
private User(int age){
this.age=age;
}
public static User getInstance(int age){
if(user==null){
synchronized (User.class){
if(user==null){
System.out.println(Thread.currentThread().getName()+"先进来");
user=new User(age);
}
}
}
return user;
}
}
单例模式的如何破坏
因为单例模式的核心是构造器的私有化,如果通过反射的方式将类的构造器设置为public,这样不就可以通过构造器实例化多个不同的实例对象了吗?
阶段1:
思路:通过类反射获取构造参数,并将构造参数设置为可见(因为是私有),然后通过反射获取的构造再去创建创建新对象。这样每次获取对象都是通过构造函数获取的,对象实例化有多个。
System.out.println(User.getInstance());// 使用单例模式的构建对象
Constructor<User> constructor = User.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance()); // 使用反射获取的对象构建对象
System.out.println(constructor.newInstance()); // 使用反射获取的对象构建对象
结果
防止这样破解怎么办?采用阶段2的方式
阶段2:
思路:在构造时,判断此类的对象是否存在。
但这种只适用于在使用反射获取对象前已经使用单例获取了对象,如果全部使用反射构造获取,类中的构造方法将无法获取不使用单例模式创建的对象,使得这种方式无效。
阶段3:
思路:无论是单列模式还是通过反射创建对象都离不开构造方法,要实现只有一个实例,只需要确保构造方法只能被调用一次即可。可以设置一个标志确保构造只会调用一次。
但这种还存在一个问题,就是通过反射去修改这个标志。如果flag不是为boolean类型,那通过返射知道要修改为什么吗?如果设置为一个密码,只有符合这个密码,才可以调用构造方式实例化一个对象,调用一次立即修改为其他值,这样就只有通过反射将这个flag修改为和密码一样才可以再实例化一个对象。
如何无法通过反射破坏单实例模式呢?可以通过枚举来构造单例模式。
4、使用枚举类
public class Application {
public static void main(String[] args) throws Exception {
System.out.println(Singleton.INSTANCE.getInstance());
System.out.println(Singleton.INSTANCE.getInstance());
}
}
enum Singleton {
INSTANCE;
private User instance;
Singleton() {
instance = new User();
}
private class User{
}
public User getInstance() {
return instance;
}
}