static关键字以及单例模式

static关键字

首先static的概念作用什么的大家都在网上看了很多了。所以这里以测试为主。主要测试静态代码块,静态变量以及静态内部类。
首先看一个测试。

package Collection;
class StaticTT {
    
    public static int A = 4;     
    
    static {     
        System.out.println("exec static");     
    }     
    
}
    
public class MyStatic {     
    public static void main(String[] args) {     
        System.out.println(StaticTT.A);     
    }     
    
}

运行结果如下:
在这里插入图片描述
先输出静态代码块的,再打印值。是不是很正常。证明已经StaticTT已经被加载了。

然后看下面的测试。A加上final。

package Collection;
class StaticTT {
    
    public static final int A = 4;     
    
    static {     
        System.out.println("exec static");     
    }     
    
}
    
public class MyStatic {     
    public static void main(String[] args) {     
        System.out.println(StaticTT.A);     
    }     
    
}

结果如下:
在这里插入图片描述
可以看到final修饰后没有输出exec static了。说明StaticTT这个类没有被加载!所以关键还是final这个关键字。
首先static关键字表示该变量是静态变量。static对象可以在他的任何对象创建之前访问,无需引用任何对象,当声明其他类的对象时,不生成static变量的副本,而是类的所有实例共享同一个static变量。
而多加了一个final后。我就理解为“全局变量”。即已经不在依赖于类了。而是独立出来了。因此调用StaticTT.A是没有加载StaticTT这个类的。这只是我的理解。不知道对不对。以后有空就证明看看。

然后就开始接下来的测试了。

package Collection;

class staticClassBoader{
	//静态变量
	public static int i=30;
	//静态方法
	private static void interIntVal() {
		System.out.println("执行静态外部类中的静态方法!");
	}
	//静态语句块
	static {
		System.out.println("执行静态外部类中的静态语句块!");
	}
}

public class StaticTest {

	//静态变量
	public static int i=10;
	//语句块
//	{
//		System.out.println("执行语句块!");
//	}
	//静态方法
	private static void intVal() {
		System.out.println("执行静态方法!");
	}
	//静态语句块
	static {
		System.out.println("执行静态语句块!");
	}
	//静态内部类
	public static class staticClass{
		//静态变量
		public static int i=10;
		//静态方法
		private static void interIntVal() {
			System.out.println("执行静态内部类中的静态方法!");
		}
		//静态语句块
		static {
			System.out.println("执行静态内部类中的静态语句块!");
		}
	}
	public static void main(String[] args) {
	}
	
}

这里就测试了静态变量,静态代码块,静态方法,静态内部类(里面包含静态变量,静态代码块,静态方法),以及外部类,外部类测试过了,这里就不测试了。

首先main方法中什么都不写。执行如下:
在这里插入图片描述
就执行了静态语句块。静态方法并没有被执行。

main中加上一行:

StaticTest.intVal();

在这里插入图片描述
这里静态方法就加载了。

在main方法中的那一行代码改为:

staticClass.interIntVal();

在这里插入图片描述上面的情况应该比较容易理解。
看下以下链接。java的static块执行时机。
https://www.cnblogs.com/ivanfu/archive/2012/02/12/2347817.html
所以可以得出一个猜想:
首先如果是外部类。如果变量static加了final关键字,就能理解为全局变量。去访问它并不会加载该类。如果没有加final关键字。就还是类中。会加载该类。
如果不是外部类。即本类和内部类。
在这里插入图片描述
由第二条可以知道本类的main就是一个静态方法。所以一定会被加载。静态代码块就一定执行。其它的static修饰都是在调用时才会加载。即:
1.静态内部类和非静态内部类一样,都是在被调用时才会被加载。
2.静态内部类其实和外部类的静态变量,静态方法一样,只要被调用了都会让本类的被加载。不过当只调用本类的静态变量,静态方法时,是不会让静态内部类的被加载。
3.调用本类的静态变量,静态方法可以让本类得到加载,不过这里静态内部类没有被加载。
4.我们其实加载静态内部类的时候,其实还会先加载本类,才加载静态内部类。

单例模式

单例模式的产生

多个线程可能操作不同实例对象。多个线程要操作同一对象,要保证对象的唯一性。

单例模式解决了什么问题

实例化过程中只实例化一次

解决的办法

1.有一个实例化的过程(只有一次)。
2.提供返回实例对象的方法。

单例模式的分类

首先看一个非单例模式的例子。

package Collection;

public class Protype {
	public static Protype getInstance() {
		return new Protype();
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(Protype.getInstance());
			}).start();
		}
	}
}

运行结果如下:
在这里插入图片描述这里可以看到输出的对象实例不一样。所以多个线程并不是操作同一个实例对象。所以才会引发出了单例模式。
单例模式从线程安全性和性能上考虑。

饿汉式

顾名思义:就是一来就得有对象产生。
代码形式如下:

package Collection;

public class HungrySingleton {
	//有一个实例化的过程(只有一次)
	private static HungrySingleton singleton=new HungrySingleton();
	//提供返回实例对象的方法
	public static HungrySingleton getInstance() {
		return singleton;
	}
	
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(HungrySingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述
可以看到多个实例就是操作同一个对象。

线程安全性:在加载的时候已经被实例化,所以只有这一次,线程安全的。
性能:没有延迟加载,好长时间不使用,会占内存。影响性能。

针对上面的性能问题所以诞生了懒加载。

懒汉式

顾名思义:就是要用的时候才生产。

package Collection;

public class LazySingleton {
	private static LazySingleton singleton=null;
	public static LazySingleton getInstance() {
		if(singleton == null) singleton=new LazySingleton();
		return singleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(LazySingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述
这里运行出来是同一个实例对象。但是它的线程是不安全的。
着手分析这一行:

if(singleton == null) singleton=new LazySingleton();

当有多个线程运行到singleton=new LazySingleton();这一行的时候。他们都已经判断出了singleton为空,但是实际上已经有多个线程运行到这里了。所以按照代码,这些已经判断为空的实例会产生新的实例,这样就会导致实例可能不唯一。
线程不安全:不能保证实例对象的唯一性
性能:性能好。

因此需要对这个办法进行改进。所以有了下面这种办法。

懒汉式+同步方法

其实就是在getInstance方法上加了synchronized来保证同步。

package Collection;

public class LazySingleton {
	private static LazySingleton singleton=null;
	public static synchronized LazySingleton getInstance() {
		if(singleton == null) singleton=new LazySingleton();
		return singleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(LazySingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述
这种办法实例对象唯一了。但是如果多个线程运行到getInstance的时候,就只能串行运行了。不管你的实例是否生产。极大的影响了性能。
线程安全:实例对象的唯一。
性能:性能非常不好。容易退化成串行模式。
由于上面的性能问题。所以需要改进。改进的办法是减少锁的持有时间,进行锁的细化。所以就产生了DCL。

DCL(Double-Check-Locking)
package Collection;

public class LazySingleton {
	private static LazySingleton singleton=null;
	public static LazySingleton getInstance() {
		if(singleton == null) {
			synchronized (LazySingleton.class) {
				if(singleton == null) 
					singleton=new LazySingleton();
			}
			
		}
		return singleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(LazySingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述
主要提一下第二次判断if(singleton == null) ,主要是因为前面说的可能多个线程进入了第一个判断。所以需要再判断一次。
有的说为什么不把synchronized放到第一个判断之前。这样的话跟方法上加锁区别就不大了。
性能:性能比较好。可以懒加载。
线程安全:实例对象唯一。
但是存在一个问题。
请看下面的一个例子。

package Collection;

public class LazySingleton {
	private int a=10;
	private int b=20;
	private static LazySingleton singleton=null;
	private LazySingleton(int m,int n){
		a=m;
		b=n;
		singleton=new LazySingleton();
	}
	private LazySingleton(){}
	public static LazySingleton getInstance() {
		if(singleton == null) {
			synchronized (LazySingleton.class) {
				if(singleton == null) 
					singleton=new LazySingleton(100,200);
			}
			
		}
		return singleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(LazySingleton.getInstance());
			}).start();
		}
	}
}

这种会存在一个有参构造会存在一个问题。就是指令重排。指令重排后的顺序是

		singleton=new LazySingleton();
		a=m;
		b=n;

当然一般是不会的这样重排的。要是把a和b换成一个较为复杂的对象就有可能发生(我们就假定发生了指令重排)。那么有可能编译之后就是先初始化singleton。当第一个访问到达singleton=new LazySingleton();时,下一步时a=m;此时又存在一个线程执行getInstance,发现不为null。结果就把这个对象拿去用。取出a和b。值就是10和20.并不是100和200!这样线程就不安全了!
所以为了规避这种现象又做了调整。

volatile+DCL
package Collection;

public class LazySingleton {
	private int a=10;
	private int b=20;
	private volatile static LazySingleton singleton=null;
	private LazySingleton(int m,int n){
		a=m;
		b=n;
		singleton=new LazySingleton();
	}
	private LazySingleton(){}
	public static LazySingleton getInstance() {
		if(singleton == null) {
			synchronized (LazySingleton.class) {
				if(singleton == null) 
					singleton=new LazySingleton(100,200);
			}
			
		}
		return singleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(LazySingleton.getInstance());
			}).start();
		}
	}
}

使用了volatile就保证了a=m;b=n;一定在singleton=new LazySingleton();之前执行。放置了指令重排自然就解决了线程安全的问题。看起来算是非常完美的解决方案。
但是在实际中却用下面的方法用的多。

holder(持有者模式)
package Collection;

public class HolderSingleton {

	private static class Holder{
		private static HolderSingleton singleton=new HolderSingleton();
	}
	public static HolderSingleton getInstance() {
		return Holder.singleton;
	}
	
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(HolderSingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述
性能:这种办法没有上锁。还是懒加载,性能高。
线程安全:第一个执行getInstance方法的线程会直接初始化HolderSingleton的一个实例,由于static关键字修饰,只能初始化一次,后面的线程就直接使用这个实例了。不会存在多个实例。是安全的。
相比于前面一种最优的解决方案。这种方法没有加锁。所以性能更好。而且线程也安全。是懒加载模式。所以它比volatile+DCL更优。

枚举
package Collection;

public class EnumSingleton {

	//生产实例
	private enum MySingleton{
		mysingleton;
		private static EnumSingleton singleton=null;//懒加载
		private EnumSingleton getInstance() {
			singleton=new EnumSingleton();
			return singleton;
		}
	}
	//返回对象
	private static EnumSingleton getInstance() {
		return MySingleton.mysingleton.getInstance();
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(EnumSingleton.getInstance());
			}).start();
		}
	}
}


更简单的写法:

package Collection;

public enum EnumSingleton {
	//生产实例
	enumSingleton;
	//返回对象
	private static EnumSingleton getInstance() {
		return enumSingleton;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				System.out.println(EnumSingleton.getInstance());
			}).start();
		}
	}
}

在这里插入图片描述在这里插入图片描述
都知道这是effective java中推荐的单例模式。毋庸置疑,它的效率肯定也是非常高的。此外它还有其它优点:
1.写法简单。
2.可以自己处理序列化。
3枚举实例创建是thread-safe。创建枚举默认就是线程安全的。
枚举的具体优点可以参考这篇文章:
https://www.cnblogs.com/linjiaxin/p/7923135.html
为什么用枚举。
https://www.cnblogs.com/chiclee/p/9097772.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值