单例设计模式
单例模式的特点
- 某个类只能有一个实例
- 它必须自行创建这个类
- 它不许向整个系统提供这个类的实例
饿汉模式/立即加载
饿汉式的特点: 在使用类时就已经将对象加载完毕
饿汉式的缺点:
- 由于在使用前就加载完毕,会造成内存资源的浪费
- 在获取实例时,若没有同步方法,容易产生非线程安全问题。
饿汉式的创建方式:
- 静态常量饿汉式
- 静态代码块饿汉式
- 枚举类
静态常量饿汉式
// 静态常量的饿汉式
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return instance;
}
}
静态代码块饿汉式
// 静态代码块的饿汉式
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
}
static {
instance = new Singleton2();
}
public static Singleton2 getInstance() {
return instance;
}
}
饿汉式的非线程安全问题
如果在获取实力式没有同步,则会产生非线程安全问题
【示例】
使用静态常量饿汉式,创建一个 Article 方法, 包含标题内容两个属性,返回一个实例对象并重写 toString 方法
public class Article {
private static Article object = new Article();
private static String title;
private static String content;
private Article() {
}
public static Article getInstance() {
title=Thread.currentThread().getName();
Date a = new Date();
content = "The Article was Written at " + a ;
System.out.println(title + content);
try {
TimeUnit.SECONDS.sleep(8);
}catch(InterruptedException e) {
e.printStackTrace();
}
return object;
}
@Override
public String toString() {
return title + " : " + content;
}
}
创建一个线程任务,并在线程中获取示例,并打印结果
public class MyThread extends Thread {
@Override
public void run() {
Article instance = Article.getInstance();
System.out.println(instance.hashCode() + " : " + instance);
}
}
Main 方法
public class Main {
public static void main(String[] args) {
MyThread threads[] = new MyThread[3];
for(int i =0 ; i< 3; i++) {
threads[i] = new MyThread();
threads[i].setName("Thread-"+(i+1));
threads[i].start();
try {
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果
Thread-1The Article was Written at Tue Dec 08 10:09:56 CST 2020
Thread-2The Article was Written at Tue Dec 08 10:09:57 CST 2020
Thread-3The Article was Written at Tue Dec 08 10:09:58 CST 2020
953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020
953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020
953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020
由上述运行结果可知,最终变量被修改成相同的。但是三个线程都是获取的同一个实例。
懒汉模式/延时加载
懒汉式的创建方式
- 线程不安全的懒汉式
- 线程安全的懒汉式
- 同步代码块懒汉式
- DCL
线程不安全懒汉式
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {
}
public static Singleton1 getInstance() {
if(instance == null) {
instance = new Singleton1();
}
return instance;
}
}
那么为何说这个是线程不安全的创建方式呢
【示例】
一个单例类
public class Instance {
private static Instance instance;
private Instance() {
System.out.println("Create a Instance");
}
public static Instance getInstance() {
if(instance == null) {
instance = new Instance();
}
return instance;
}
}
一个任务类,用于获取单例实例
public class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "Start");
Instance instance = Instance.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + instance.hashCode());
try {
TimeUnit.SECONDS.sleep(3);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "Finished
with : " + Instance.getInstance().hashCode());
}
}
Main 方法
public class Main {
public static void main(String[] args) {
Task task = new Task();
Thread thread1 = new Thread(task,"Thread-1");
Thread thread2 = new Thread(task,"Thread-2");
Thread thread3 = new Thread(task,"Thread-3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果
Thread-2Start
Thread-3Start
Thread-1Start
Create a Instance
Create a Instance
Create a Instance
Thread-2 : 469684341
Thread-3 : 55699698
Thread-1 : 643939211
Thread-1Finished with : 643939211
Thread-2Finished with : 643939211
Thread-3Finished with : 643939211
由运行结果可知,在此过程中创建了3个实例,这便是产生了线程安全问题,三个线程同时访问指针获取空值,从而创建了三个变量。
线程安全的懒汉式
为了保证线程安全,我们可以有两种方法,一种是为方法加锁,另一种方法便是使用同步代码块
假设为获取实例的方法声明为同步方法,那么方法里若有许多与创建实例无关的代码必然会影响代码运行的效率。
为了保证运行效率,我们可以将创建的关键步骤放入同步代码块中
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
}
public static Singleton2 getInstance() {
// 其他步骤
try {
synchronized(Singleton2.class) {
if(instance == null) {
// 创建前的步骤
instance = new Singleton2();
}
}
}catch(Exception e) {
e.printStackTrace();
}
return instance;
}
}
当然,到这一步,那么以上代码还能否进一步提升运行效率呢
DCL 双检查机制
public class Singleton3 {
private volatile static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
// 其他代码
try {
if(instance == null) {
// 创建前的准备代码
synchronized(Singleton3.class) {
if(instance==null) {
instance = new Singleton3();
}
}
}
}catch(Exception e) {
e.printStackTrace();
}
return instance;
}
}
双检查机制,将创建前的代码从创建代码分离出去,减少了锁占有的时间,减少了等待时间,既保证了效率,也避免了多次创建的线程安全问题。
PS: 对于需要返回的instance 实例需要加上 volatile 关键字,以保证其内存可见性。避免由于指令重排导致的错误。
内部静态类
public class Singleton1 {
private static class SingletonHandler{
private SingletonHandler() {
System.out.println("Create SingletonHandler");
}
private static Singleton1 instance = new Singleton1();
}
private Singleton1() {
System.out.println("Create Singleton1");
}
public static Singleton1 getInstance() {
return SingletonHandler.instance;
}
}
枚举类
枚举类与静态代码块的特征比较类似
public enum Singleton1 {
instance("instance"),
check("check");
private String instanceString;
Singleton1(String str){
instanceString = str;
System.out.println("Create " + instanceString);
}
public String getInstance() {
return instanceString;
}
}
获取枚举类对象的调用方法
public class Main {
public static void main(String[] args) {
System.out.println(Singleton1.instance.getInstance());
}
}