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