单例模式(Java)
单例模式分为饿汉式和懒汉式
饿汉式实现比较简单
//饿汉式
class SingleTon1{
private static SingleTon1 instance=new SingleTon1();
private SingleTon1(){}
public static SingleTon1 getInstance(){
return instance;
}
}
饿汉式存在的优缺点:
优点是不用考虑多线程问题,因为在类加载时已初始化。
缺点:1.长期占用一块内存,存在资源浪费;2.程序启动时需创建对象,会影响启动速度。
懒汉式
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
public static SingleTon2 getInstance(){
if(instance == null){
instance=new SingleTon2();
}
return instance;
}
}
测试
SingleTon2 instance1 = SingleTon2.getInstance();
System.out.println(instance1);
SingleTon2 instance2 = SingleTon2.getInstance();
System.out.println(instance1);
返回相同的地址,实现了单例
多线程测试
class myrunable implements Runnable{
@Override
public void run(){
SingleTon2 instance1 = SingleTon2.getInstance();
System.out.println(instance1);
}
}
//多线程测试
for (int i=0;i<10;i++){
Thread th=new Thread(new myrunable());
th.start();
}
测试结果发现存在不同的地址,这是因为多个线程同时访问getInstance时,new了多次。需增加多线程锁控制多线程创建对象
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
synchronized public static SingleTon2 getInstance(){
if(instance == null){
instance=new SingleTon2();
}
return instance;
}
}
class myrunable implements Runnable{
@Override
public void run(){
SingleTon2 instance1 = SingleTon2.getInstance();
System.out.println(instance1);
}
}
public class MySingleTon {
public static void main(String[] args) {
//多线程测试
for (int i=0;i<10;i++){
Thread th=new Thread(new myrunable());
th.start();
}
}
}
测试发现返回地址变成了1个,说明满足多线程时,创建同一对象的要求。但是直接在该方法中增加锁,会影响多线程访问效率。需优化
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
public static SingleTon2 getInstance(){
synchronized(SingleTon2.class){
if(instance == null){
instance=new SingleTon2();
}
}
return instance;
}
}
class myrunable implements Runnable{
@Override
public void run(){
SingleTon2 instance1 = SingleTon2.getInstance();
System.out.println(instance1);
}
}
public class MySingleTon {
public static void main(String[] args) {
// SingleTon2 instance1 = SingleTon2.getInstance();
// System.out.println(instance1);
// SingleTon2 instance2 = SingleTon2.getInstance();
// System.out.println(instance1);
//多线程测试
for (int i=0;i<10;i++){
Thread th=new Thread(new myrunable());
th.start();
}
}
}
在这个 getInstance
方法中,synchronized
关键字是用来锁定 SingleTon2.class
这个类的 Class 对象。这意味着在同一时间,只有一个线程能够进入 synchronized
代码块,其他尝试进入该代码块的线程将被阻塞,直到锁被释放。
这里的 synchronized(SingleTon2.class)
确保了在多线程环境下,instance
变量的初始化是线程安全的。也就是说,如果多个线程同时调用 getInstance
方法,它们将串行地执行 synchronized
代码块内的代码,从而确保 instance
只会被初始化一次。
具体来说,synchronized(SingleTon2.class)
的作用是:
- 当一个线程进入
synchronized
代码块时,它会获取SingleTon2.class
的锁。 - 如果此时有其他线程试图进入
synchronized
代码块,它们将因为无法获取锁而被阻塞。 - 当第一个线程执行完
synchronized
代码块内的代码并退出时,它会释放锁,此时其他线程中的一个将有机会获取锁并继续执行。
这种实现方式被称为“双重检查锁定”(double-checked locking),是一种常用的单例模式实现方法,用于在延迟初始化时提高性能。但是,这种实现方式在 Java 5 及更早版本中可能由于指令重排而出现问题。从 Java 5 开始,通过引入 volatile
关键字可以确保双重检查锁定的正确性。因此,为了完全确保线程安全,你可能会看到 instance
变量被声明为 volatile
:
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
public static SingleTon2 getInstance(){
synchronized(SingleTon2.class){
if(instance == null){
instance=new SingleTon2();
}
}
return instance;
}
}
class myrunable implements Runnable{
@Override
public void run(){
SingleTon2 instance1 = SingleTon2.getInstance();
System.out.println(instance1);
}
}
public class MySingleTon {
public static void main(String[] args) {
//多线程测试
for (int i=0;i<10;i++){
Thread th=new Thread(new myrunable());
th.start();
}
}
}
但是这样处理,会造成多线程访问的时候每次都被锁定,需优化
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
public static SingleTon2 getInstance(){
if(instance == null){
synchronized(SingleTon2.class){
instance=new SingleTon2();
}
}
return instance;
}
}
此时又会遇到新的问题,如果有2个线程走到了
if(instance == null){
synchronized(SingleTon2.class){
instance=new SingleTon2();
}
}
其中A线程阻塞,B线程锁定,那么B执行完之后,A进入还是会new,所以需要再增加一层判断
class SingleTon2{
public static SingleTon2 instance;
private SingleTon2(){}
public static SingleTon2 getInstance(){
if(instance == null){
synchronized(SingleTon2.class){
if(null == instance){
instance=new SingleTon2();
}
}
}
return instance;
}
}
这就实现了双重检测锁
此方式优缺点分析:优点:实现延迟初始化和线程安全;缺点:多线程竞争资源时,仍然面临性能问题。有没有既能解决延迟初始化又能解决线程安全的单例,有:
class SingleTon2{
private static class SingleInner{
public static SingleTon2 instance=new SingleTon2();
}
public static SingleTon2 getInstance(){
return SingleInner.instance;
}
}
需要的时候才会初始化