定义概述:
单例模式是设计模式中一种,它的要求是单例模式实现的类,必须保证全局中只能实例出一个对象,即在全局中只能存在一个该类的对象。实现方式有两种:饿汉式、懒汉式。
使用场景
- 网站浏览次数的计数器,一般使用单例模式设计。因为如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样在多线程环境下实计数的值是难以同步的。
- windows回收站,你可以尝试打开windows回收站,第一次打开弹出新建回收站窗口,但是第二次打开,不会再打开新的窗口了,只会出现已经打开的窗口。
特征:
单例类都有如下特征
- 私有的构造方法
- 指向自身的静态实例声明
- 公有的获取自身实例的静态方法
实现:
1. 一般情况下类的写法。
- 我们会调用默认的构造方法,生成类的实例,此时会生成两个person对象实例
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class MainClass {
public static void main(String[] args) {
//调用默认构造函数,创建了两个对象
Person per1 = new Person();
Person per2 = new Person();
per1.setName("test1");
per2.setName("test2");
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
2. 饿汉式
- 饿汉式单例顾名思义,当类被加载的时候,就会创建一个该类的实例对象。其优点是不会引起线程安全问题。代码如下:
public class PersonEager {
//创建一个全局静态常量对象。在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
//多线程环境下可以保证实例唯一
private static final PersonEager personEager = new PersonEager();
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//私有构造函数,使得外部不能随意访问构造函数创建对象
private PersonEager(){
}
//提供一个公有方法,返回全局静态常量对象。实现单例(即一个对象,全局只有一个实例)
public static PersonEager getPersonEager(){
return personEager;
}
}
- 可以看到饿汉式的代码申明了一个自身实例的静态常量私有属性,由于静态属性在类被加载的时候就会初始化,并且Java中类只会加载一次,所以就保证了PersonEager类只会被实例化一次,所以就不存在了线程安全问题。测试代码如下:
public class MainClass {
public static void main(String[] args) {
//使用饿汉式,实现全局唯一实例
PersonEager per3 = PersonEager.getPersonEager();
PersonEager per4 = PersonEager.getPersonEager();
per3.setName("test3");
per4.setName("test4");
System.out.println(per3.hashCode());
System.out.println(per4.hashCode());
}
}
下面用20个线程去测试饿汉式是否有线程安全问题,代码如下:
public class MainClassThread {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable(){
public void run(){
System.out.println(Thread.currentThread().getName() + "-" +PersonEager.getPersonEager().hashCode());
}
}).start();
}
}
}
测试结果如下,可以看到创建的都是一个对象,并没有出现线程安全问题。
- 但是饿汉式也有缺点,那就是类只要被加载了就会被实例化,虽然加快了下次调用的时间(不用人为判断是否需要重新实例化),但是也浪费资源空间,典型的以资源换时间。因为可能某个类被加载后,就一直不会被用到。所以就衍生出了下面的懒汉式。
3. 懒汉式
- 懒汉式的基本原理是保证实例化一个对象,而且是按需加载,即只有在真正申明实例化该对象的时候才会去实例化这个对象。代码如下:
public class PersonLazy {
private static int count;
//创建一个全局静态对象。静态对象会在类加载的时候创建,但此处是创建的null 对象,目的是实现延迟加载
//多线程环境中无法保证实例唯一
private static PersonLazy personLazy = null;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//私有构造函数,使得外部不能随意访问构造函数创建对象
private PersonLazy(){
System.out.println("PersonLazy 私有的构造方法被实例化 " + (++count) + " 次。");
}
//提供一个公有方法,通过判断对象是否被实例化,返回全局静态对象,实现单例(即一个对象,全局只有一个实例)。
//在调用此方法时,才创建实例。
//此处无法保证多线程环境下,实例唯一
public static PersonLazy getPersonLazy(){
if(personLazy == null){
personLazy = new PersonLazy();
}
return personLazy;
}
}
- 可以看到懒汉式上面的代码中getPersonLazy()方法在单线程环境中没有问题,但是在多线程环境下会出现线程安全问题。我们分别测试。
- 懒汉式单线程环境测试:
public class MainClass {
public static void main(String[] args) {
//懒汉式,单线程测试
PersonLazy per5 = PersonLazy.getPersonLazy();
PersonLazy per6 = PersonLazy.getPersonLazy();
per5.setName("test5");
per6.setName("test6");
System.out.println(per5.hashCode());
System.out.println(per6.hashCode());
}
}
- 懒汉式多线程环境测试,20000个线程去测试:
public class MainClassThread {
public static void main(String[] args) throws InterruptedException {
Runnable task = ()->{
String threadName = Thread.currentThread().getName();
System.out.println("线程 " + threadName + "\t => " + PersonLazy.getPersonLazy().hashCode());
};
// 模拟多线程环境下使用 PersonLazy 类获得对象
for(int i=0;i<20000;i++){
new Thread(task,"" + i).start();
}
}
}
下面是多线程环境下懒汉式测试结果,可以看到构造函数被调用过两次,也导致了创建了两个对象。
3.1. 懒汉式--方法同步锁
- 解决上述线程同步问题,可以一种方式是加上方法同步锁,但是这种方式由于锁的范围大,导致了效率低下,代码如下。后续会测试这种方法的效率。
//加上同步,保证线程安全。实现实例唯一。方法同步锁,效率不高
public static synchronized PersonLazy getPersonLazySync(){
if(personLazy == null){
personLazy = new PersonLazy();
}
return personLazy;
}
- 测试代码如下:
public class MainClassThread {
public static void main(String[] args) throws InterruptedException {
//懒汉式多线程测试--线程安全--效率不高
Runnable task = ()->{
String threadName = Thread.currentThread().getName();
System.out.println("线程 " + threadName + "\t => " + PersonLazy.getPersonLazySync().hashCode());
};
// 模拟多线程环境下使用 PersonLazy 类获得对象
for(int i=0;i<20000;i++){
new Thread(task,"" + i).start();
}
}
}
-
测试结果
3.2. 懒汉式--双重检查
- 双重检查代码如下,可以看到getPersonLazySyncDoubleCheck()方法中,判断了personLazy == null两次。保证了同步锁竞争只有在第一次初始化的时候才会发生,以后再使用实例的时候,在第一重检查就跳过了同步锁。
- personLazy 变量必须用volatile 修饰,因为java中new操作并非原子性操作,多线程环境下可能会导致对象尚未被完全创建。
public class PersonLazy {
private static int count;
//创建一个全局静态对象。静态对象会在类加载的时候创建,但此处是创建的null 对象,目的是实现延迟加载
//使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
private static volatile PersonLazy personLazy = null;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//私有构造函数,使得外部不能随意访问构造函数创建对象
private PersonLazy(){
System.out.println("PersonLazy 私有的构造方法被实例化 " + (++count) + " 次。");
}
//局部线程安全,双重检查。效率高,但是方法不简洁
public static PersonLazy getPersonLazySyncDoubleCheck(){
//第一重检查
if(personLazy == null){
synchronized (PersonLazy.class){
//第二重检查
if(personLazy == null){
personLazy = new PersonLazy();
}
}
}
return personLazy;
}
}
- 测试代码如下:
public class MainClassThread {
public static void main(String[] args) throws InterruptedException {
//懒汉式多线程测试--线程安全--效率不高
Runnable task = ()->{
String threadName = Thread.currentThread().getName();
System.out.println("线程 " + threadName + "\t => " + PersonLazy.getPersonLazySyncDoubleCheck().hashCode());
};
// 模拟多线程环境下使用 PersonLazy 类获得对象
for(int i=0;i<20000;i++){
new Thread(task,"" + i).start();
}
}
}
-
测试结果
3.3. 懒汉式--内部类
- 内部类实现是单例的一种实现,它不仅能实现线程安全,并且也保证了效率。
- 如下代码所示,它在内部有一个静态的内部类Holder,然后在这个内部类内申明了外部类的实例。根据类的加载机制,当外部类被初始化的时候,它的静态内部类不会被初始化,所以就达到了延迟加载的目的。
- 另外,当调用getPersonLazyInnerClass()方法时,JVM才会去初始化Holder,在初始化的一系列操作中,其中就包括了初始化其静态属性的操作。且《深入理解java虚拟机》中有提到
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。
- 故而,静态内部类由JVM保证了线程的安全性。
public class PersonLazyInnerClass {
private static int count;
// 私有内部类,按需加载,用时加载,也就是延迟加载
private static class Holder{
private static PersonLazyInnerClass personLazyInnerClass = new PersonLazyInnerClass();
}
private PersonLazyInnerClass(){
System.out.println("PersonLazy 私有的构造方法被实例化 " + (++count) + " 次。");
}
public static PersonLazyInnerClass getPersonLazyInnerClass(){
return Holder.personLazyInnerClass;
}
}
- 测试代码如下:
public class MainClassThread {
public static void main(String[] args) throws InterruptedException {
//懒汉式内部类--线程安全--效率高
Runnable task = ()->{
String threadName = Thread.currentThread().getName();
System.out.println("线程 " + threadName + "\t => " + PersonLazyInnerClass.getPersonLazyInnerClass().hashCode());
};
// 模拟多线程环境下使用 PersonLazy 类获得对象
for(int i=0;i<20000;i++){
new Thread(task,"" + i).start();
}
}
}
-
测试结果
效率测试:
- 测试各种情况下,调用其创建单例方法1千万次,程序执行时间。最终测试结果如下:
方法同步锁 -------- 2339ms
双重检查 -------- 8ms
内部类 ------- 12ms
- 测试结果内部类和双重检查区别不是很大。
测试代码如下:
public class MainClassEffc {
public static void main(String[] args) throws InterruptedException {
long startTime=System.currentTimeMillis();
//效率测试-方法同步锁
/*Thread t1 = new Thread(new Runnable(){
public void run(){
for (int i = 0; i < 100000000; i++) {
PersonLazy.getPersonLazySync();
//System.out.println(Thread.currentThread().getName() + "-" +PersonLazy.getPersonLazySync().hashCode());
}
}
});*/
Thread t1 = new Thread(new Runnable(){
public void run(){
for (int i = 0; i < 100000000; i++) {
PersonLazy.getPersonLazySyncDoubleCheck();
//System.out.println(Thread.currentThread().getName() + "-" +PersonLazy.getPersonLazySync().hashCode());
}
}
});
/*Thread t1 = new Thread(new Runnable(){
public void run(){
for (int i = 0; i < 100000000; i++) {
PersonLazyInnerClass.getPersonLazyInnerClass();
//System.out.println(Thread.currentThread().getName() + "-" +PersonLazy.getPersonLazySync().hashCode());
}
}
});*/
t1.start();
t1.join();
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("结束执行任务!!!!" + (endTime-startTime) + "ms");
}
}