目录
# 1.Paths:
# 2.依赖注入:
# 3.java使用java.lang.management监视和管理 Java 虚拟机
# 4.Iterator和Iterable的区别
# 5.java 替换反斜杠问题:
# 6.静态成员
# 7.ThreadLocal
# 8.java8中的LocalDate,LocalTime,LocalDateTime与Date的转换和使用
# 9.strictfp 关键字
# 10.spring里的cron表达式0 0/10 * * * 与 0 */10 * * *区别
# 11.Try-with-resources
# 12.YYYY和yyyy
# 13. UUID
# RunTime.getRunTime().addShutdownHook
# 为什么使用token?session与token的区别
“Comparison method violates its general contract!”问题原因及解决办法
单例模式简介
# 1.Paths:
Paths.get(s).normalize(); get方法返回拼接字符串后的路径。可以是一个参数,也可以是多个。normalize方法是返回删除冗余名称元素的路径。 在许多文件系统中,“ .
”和“ ..
”是用于指示当前目录和父目录的特殊名称。 在这样的文件系统中,所有单独出现的“ .
”和“..”都被认为是冗余的,但是如下面ss..一起出现就不会删除。 如果“ ..
”前面有一个非“ ..
”的名称,那么这两个名称都被认为是冗余的(识别这些名称的过程被重复,直到它不再适用),只删除“ ..
”和前面的一个元素。
此方法不访问文件系统; 该路径可能找不到存在的文件。 从路径中删除“ ..
”和前面的名称可能导致定位与原始路径不同的文件的路径。
getParrent方法返回父路径:
File中getPath(),getAbsolutePath(),getCanonicalPath()区别:
File file = new File(".\\test.txt");
System.out.println(file.getPath());//.\test.txt
System.out.println(file.getAbsolutePath());//E:\idea\.\test.txt
System.out.println(file.getCanonicalPath());//E:\idea\test.txt
File file2 = new File("E:\\idea\\.\\test.txt");
System.out.println(file2.getPath());//E:\idea\.\test.txt
getPath():
返回的是定义时的路径,可能是相对路径,也可能是绝对路径,这个取决于定义时用的是相对路径还是绝对路径。如果定义时用的是绝对路径,那么使用getPath()返回的结果跟用getAbsolutePath()返回的结果一样
getAbsolutePath():
返回的是定义时的路径对应的绝对路径,但不会处理“.”和“..”的情况
getCanonicalPath():
返回的是规范化的绝对路径,相当于将getAbsolutePath()中的“.”和“..”解析成对应的正确的路径.
Xxx.class.getResource(""),Xxx.class.getResource("/") ,Xxx.class.getClassLoader().getResource("")的区别
首先明确一点:三个方法找的都是该项目classpath路径下的文件,也就是.java编译后的.class文件所在的目录,因为运行的时.class文件。idea中可以把配置文件加再classpath中来运行时读取,
方式:
添加成功后iml的配置文件里会有
public static void main(String[] args)
{
// 当前类(class)所在的包目录 file:/E:/idea/out/production/idea/resource/
System.out.println(ResourceTest.class.getResource(""));
// class path根目录 file:/E:/idea/out/production/idea/
System.out.println(ResourceTest.class.getResource("/"));
// class path根目录 file:/E:/idea/out/production/idea/
System.out.println(ResourceTest.class.getClassLoader().getResource(""));
//file:/E:/idea/out/production/idea/Cl.obj
System.out.println(ResourceTest.class.getResource("/Cl.obj"));
//file:/E:/idea/out/production/idea/DiffClass.obj
System.out.println(ResourceTest.class.getClassLoader().getResource("DiffClass.obj"));
//file:/E:/idea/out/production/idea/resource/ClassTest.java
System.out.println(ResourceTest.class.getResource("/resource/ClassTest.java"));
//file:/E:/idea/out/production/idea/resource/Color.java
System.out.println(ResourceTest.class.getClassLoader().getResource("resource/Color.java"));
}
Xxx.class.getResource(""):获取当前类的.class文件目录下的文件名(参数),上面的例子是再idea/resource目录下
Xxx.class.getResource("/"):与上面的区别时以“/”开头,它是去对应的class文件的根目录下找目标文件,上面的例子是idea下。
Xxx.class.getClassLoader().getResource("") == Xxx.class.getResource("/") 此种方法不能又“/”。
三种最后真正调用的都是getClassLoader().getResource(“”)方法,只是class.getResource("")再调用前判断了是不是以“/”开头,是就去找classPath的根目录找,不是就去此类所在的包路径下找,三者路径都要正确才可以,否则返回null。
Class和ClassLoader的getResourceAsStream()方法再读取路径上根上面规则一样,只是返回的是InputStream.
# 2.依赖注入:
import javax.inject.Inject 依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在 传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者 实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
@Inject支持构造函数、方法和字段注解,也可能使用于静态实例成员。可注解成员可以是任意修饰符(private,package-private,protected,public)。注入顺序:构造函数、字段,然后是方法。父类的字段和方法注入优先于子类的字段和方法,同一类中的字段和方法是没有顺序的。@Inject注解的构造函数可以是无参或多个参数的构造函数。@Inject每个类中最多注解一个构造函数。 import javax.inject.Inject @Inject
在字段注解:
- 用@Inject注解
- 字段不能是final的
- 拥有一个合法的名称
在方法上注解:
- 用@Inject注解
- 不能是抽象方法
- 不能声明自身参数类型
- 可以有返回结果
- 拥有一个合法的名称
- 可以有0个或多个参数
# 3.java使用java.lang.management监视和管理 Java 虚拟机
提供管理接口,用于监视和管理 Java 虚拟机以及 Java 虚拟机在其上运行的操作系统
java使用java.lang.management监视和管理 Java 虚拟机_程序员之路
# 4.Iterator和Iterable的区别
1. 在jdk 1.5以后,引入了Iterable,使用forEach语句(增强型for循环)必须实现Iterable接口。
2. Java设计者让Collection继承于Iterable接口而不是Iterator接口。首先要明确的是,Iterable的子类Collection,Collection的子类List,Set等,这些是数据结构或者说放数据的地方。Iterator是定义了迭代逻辑的对象,让迭代逻辑和数据结构分离开来,这样的好处是可以在一种数据结构上实现多种迭代逻辑。
3. 更重要的一点是:每一次调用Iterable的Iterator()方法,都会返回一个从头开始的Iterator对象,各个Iterator对象之间不会相互干扰,这样保证了可以同时对一个数据结构进行多个遍历。这是因为每个循环都是用了独立的迭代器Iterator对象。
# 5.java 替换反斜杠问题:
1、 java字符串或者char中,表示反斜杠,都用\\,两个反斜杠表示。因为根据java语言规范,unicode字符用\uxxxx表示,比如汉字“你”的unicode值是“\u4f60”,所以,为了避免被解释为unicode字符,用双斜线。
2、java的正则表达式中,对于反斜杠用两个反斜杠表示。
根据以上两条,得出
如果是字符串,那么\\表示\
如果是正则表达式,那么\\\\表示\
解释二
1、反斜杠(\)属于元字符,在字符串中表示时,必须转义,所以是\\
2、在正则表达式中,元字符要进行双重转义,比如\,转义一次是\\,第二次转义就是\\\\
# 6.静态成员
静态static变量/方法在类加载的过程中被初始化,在内存中只存在一份,所以可以把它当作是全局变量/方法。
优点
- 属于类级别的,不需要创建对象就可以直接使用.
- 全局唯一,内存中唯一,静态变量可以唯一标识某些状态.
- 在类加载时候初始化,常驻在内存中,调用快捷方便.
应用场景:
1. 静态方法最适合工具类中方法的定义;比如文件操作,日期处理方法等.
2. 静态方法适合入口方法的定义;如单例模式,因为从外部拿不到构造函数,所有定义一个静态的方法获取对象非常有必要.
3. 静态变量适合全局变量的定义.(如布尔型静态成员变量做控制符)
缺点
1.静态方法不能调用非静态的方法和变量.(非静态方法可以任意的调用静态方法/变量)
2.不能使用this和super关键字(属于类级别,没有创建对象签不可用this/super)
3.静态变量一旦定义,将一直存在于整个系统运行的整个过程,java垃圾回收机制,永远不会回收它占用的内存,定义过多必然造成大量占用java虚拟机的内存,影响系统的数据处理过程,甚者造成内存溢出
java 的方法中只允许final static 的 静态变量,非final 的静态变量不能有。
成员静态变量:线程非安全,静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
# 7.ThreadLocal
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,ThreadLocal并不是一个Thread,而是Thread的局部变量,
在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
ThreadLocal是什么:
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,只要线程是活动的,变量就可以访问; 线程消失后,其所有副本线程的本地实例会进行垃圾回收(除非其他情况存在对这些副本的引用)。
ThreadLocal 的作用:
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。每个线程都保留对其本地线程副本的隐式引用,
ThreadLoacal使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
ThreadLocal怎么用:既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class ThreadLocalTest {
public static void main(String [] args)
{
ThreadLocal<String> threadLocal = new ThreadLocal<>();
Random random = new Random();
IntStream.range(0, 5).forEach(a -> new Thread(() -> {
System.out.println("before线程和local值分别是" + threadLocal.get());
threadLocal.set(a + " " + random.nextInt(10));
System.out.println("after线程和local值分别是" + threadLocal.get());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start());
}
/*before线程和local值分别是null
before线程和local值分别是null
before线程和local值分别是null
before线程和local值分别是null
before线程和local值分别是null
after线程和local值分别是1 9
after线程和local值分别是3 7
after线程和local值分别是0 6
after线程和local值分别是2 1
after线程和local值分别是4 5*/
}
从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。
这就是TheadLocal的基本使用,是不是非常的简单。那么为什么会在数据库连接的时候使用的比较多呢?
class ConnectionManager
{
private static Connection connect;
public static Connection openConnection() throws SQLException {
if(connect == null)
connect = DriverManager.getConnection("");
return connect;
}
public static void closeConntction() throws SQLException {
if(connect != null)
connect.close();
}
}
上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。
ThreadLocal源码分析
set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//省略
}
我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
还有一个getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。
get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。即调用initialValue方法。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
从我们的map移除即可。
OK,其实内部源码很简单,现在我们总结一波
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
1、Thread中有一个map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
ThreadLocal为什么会内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。、
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key
下面我们分两种情况讨论:
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
基本概念
时刻:所有计算机系统内部都用一个整数表示时刻,这个整数是距离格林尼治标准时间1970年1月1日0时0分0秒的毫秒数,可以理解时刻就是绝对时间,它与时区无关,不同时区对同一时刻的解读,即年月日时分秒是不一样的;
时区:同一时刻,世界上各个地区的时间可能是不一样的,具体时间与时区有关,一共有24个时区,英国格林尼治是0时区,北京是东八区,也就是说格林尼治凌晨1点,北京是早上9点;
年历:我们都知道,中国有公历和农历之分,公历和农历都是年历,不同的年历,一年有多少月,每月有多少天,甚至一天有多少小时,这些可能都是不一样的,我们主要讨论公历。
Instant:它代表的是时间戳,表示时刻,不直接对应年月日信息,需要通过时区转换
LocalDateTime: 表示与时区无关的日期和时间信息,不直接对应时刻,需要通过时区转换
LocalDate:表示与时区无关的日期,与LocalDateTime相比,只有日期信息,没有时间信息
LocalTime:表示与时区无关的时间,与LocalDateTime相比,只有时间信息,没有日期信息
ZonedDateTime: 表示特定时区的日期和时间
ZoneId/ZoneOffset:表示时区
ZonedDateTime:
是具有时区的日期时间的不可变表示
Date的getTime方法返回的是自1970-01-01 00:00:00开始的毫秒数,它实际上getTime()与程序真实运行的容器(服务器)所在的时区相关。如果程序运行在东八区,它返回北京时间1970年01月01日08时00分00秒起至现在东八区时间的总毫秒数。new Date的时候其实调用的是System.currentTimeMillis()
/**
* 跟据偏移获取是否需要刷新数据, System.currentTimeMillis()或new Date().getTime()时间戳
* @param time1 上一次刷新时间 毫秒
* @param time2 要对比的时间 毫秒
* @param offset 偏移
* @return true 代表需要刷新
*/
public static boolean isNeedRefreshByOffset(long time1, long time2, int offset)
{
long lastDay = getDayByTimeOffset(time1, offset);
long nowDay = getDayByTimeOffset(time2, offset);
return lastDay != nowDay;
}
/**
* 根据偏移获取天数
* @param time
* @param offset
* @return
*/
public static long getDayByTimeOffset(long time, int offset)
{
return (time + getLocalTimeOffset() - offset * HOUR_MILLIS) / DAY_MILLIS;
}
/**
* 获取本地时间偏移 东八时区比0时区早8小时
//System.currentTimeMillis()获取的是GMT时间,也就是基于1970/01/01 08:00:00 的时间
//所以想要基于本地时区0点,提早8小时,就要加上本地时区的偏移时间。
* @return
*/
private static int getLocalTimeOffset()
{
return Calendar.getInstance().get(Calendar.ZONE_OFFSET);
}
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.TimeZone;
public class DateTest {
private final static String FORMAT_PATTERN1 = "yyyy-MM-dd HH:mm:ss";
private final static String FORMAT_PATTERN2 = "yyyyMMddHHmmss";
private final static String FORMAT_PATTERN3 = "yyyy-MM-dd";
private final static String FORMAT_PATTERN4 = "yyyyMMdd";
private final static String FORMAT_PATTERN5 = "HH:mm:ss";
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd : HH:mm:ss");
Date data1 = format.parse("2020-09-14 : 12:08:31");
Date data2 = format.parse("2020-09-14 : 18:15:41");
System.out.println(data1.getTime());
System.out.println(data2.getTime());
//before();
after();
}
public static void before()
{
try {
//获取Date对象,存放的是时间戳
Date date = new Date();
//获取时间戳(毫秒)
long seconds = date.getTime();
System.out.println("当前时间戳: " + seconds);
//当前GMT(格林威治)时间、当前计算机系统所在时区的时间
SimpleDateFormat beijingFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("本地(东八区)时间: " + beijingFormat.format(date));
beijingFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
System.out.println("GMT时间: " + beijingFormat.format(date));
//东八区时间转换成东九区(东京)时间,比北京早一个小时
SimpleDateFormat tokyoFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
tokyoFormat.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println("东京(东九区)时间: "+tokyoFormat.format(date));
//时间戳转化成Date
SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String fotmatString = timestampFormat.format(seconds);
Date parseDate = timestampFormat.parse(fotmatString);
System.out.println("时间戳转化成Date之后的时间: "+parseDate + ";格式化之后的: "+ fotmatString);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void after()
{
//获取当前时区的日期
LocalDate localDate = LocalDate.now();
System.out.println("localDate: " + localDate);
//时间
LocalTime localTime = LocalTime.now();
System.out.println("localTime: " + localTime);
//根据上面两个对象,获取日期时间
LocalDateTime localDateTime = LocalDateTime.of(localDate,localTime);
System.out.println("localDateTime: " + localDateTime);
//使用静态方法生成此对象
LocalDateTime localDateTime2 = LocalDateTime.now();
System.out.println("localDateTime2: " + localDateTime2);
//格式化时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss");
System.out.println("格式化之后的时间: " + localDateTime2.format(formatter));
//转化为时间戳(秒)
long epochSecond = localDateTime2.toEpochSecond(ZoneOffset.of("+8"));
//转化为毫秒
long epochMilli = localDateTime2.atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli();
System.out.println("时间戳为:(秒) " + epochSecond + "; (毫秒): " + epochMilli);
//时间戳(毫秒)转化成LocalDateTime
Instant instant = Instant.ofEpochMilli(epochMilli);
LocalDateTime localDateTime3 = LocalDateTime.ofInstant(instant, ZoneOffset.systemDefault());
System.out.println("时间戳(毫秒)转化成LocalDateTime: " + localDateTime3.format(formatter));
//时间戳(秒)转化成LocalDateTime
Instant instant2 = Instant.ofEpochSecond(epochSecond);
LocalDateTime localDateTime4 = LocalDateTime.ofInstant(instant2, ZoneOffset.systemDefault());
System.out.println("时间戳(秒)转化成LocalDateTime: " + localDateTime4.format(formatter));
}
public static void exchange()
{
Date date1 = new Date();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(localDateFormat(LocalDate.now(), FORMAT_PATTERN3));
System.out.println(localDateTimeFormat(LocalDateTime.now(),FORMAT_PATTERN1));
System.out.println(localDateTimeFormat(LocalDateTime.now(),FORMAT_PATTERN2));
System.out.println(localDateTimeToDate(LocalDateTime.now()));
System.out.println(dateFormat(new Date(), FORMAT_PATTERN4));
System.out.println(minusToMillsLocalDateTime(LocalDateTime.now(),LocalDateTime.now().minusSeconds(1)));
System.out.println(minusToMillsDate(date1,new Date()));
System.out.println(localDateParse("2018-06-12",FORMAT_PATTERN3));
System.out.println(localDateTimeParse("2018-06-12 16:04:43",FORMAT_PATTERN1));
Period p= periodDate(date1, new Date());
System.out.println("year:"+p.getYears()+"month:"+p.getMonths()+"day:"+p.getDays());
System.out.println("----------------------------------------------------------------");
Date date2= localDateToDate(LocalDate.now().minusMonths(1).minusDays(2));
Date date3= localDateToDate(LocalDate.now());
Period p2=periodDate(date2,date3);
System.out.println("year:"+p2.getYears()+"month:"+p2.getMonths()+"day:"+p2.getDays());
System.out.println("----------------------------------------------------------------");
Period p1= periodLocalDate(LocalDate.now().minusDays(2),LocalDate.now());
System.out.println("year:"+p1.getYears()+"month:"+p1.getMonths()+"day:"+p1.getDays());
Instant ins = Instant.now();
System.out.println(ins);
ZonedDateTime zoneDateTime = ins.atZone(ZoneId.systemDefault());
System.out.println(zoneDateTime);
OffsetDateTime time = ins.atOffset(ZoneOffset.ofHours(8));
System.out.println(time);
System.out.println(new Date());
}
/**
* 将localDate 按照一定的格式转换成String
* @param localDate
* @param pattern
* @return
*/
public static String localDateFormat(LocalDate localDate,String pattern){
return localDate.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 将localDateTime 按照一定的格式转换成String
* @param localDateTime
* @param pattern
* @return
*/
public static String localDateTimeFormat(LocalDateTime localDateTime,String pattern){
return localDateTime.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 将LocalDateTime 转换成 Date
* @param localDateTime
* @return
*/
public static Date localDateTimeToDate(LocalDateTime localDateTime){
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = localDateTime.atZone(zoneId);
return Date.from(zdt.toInstant());
}
/**
* 将date转换成String
* @param date
* @param pattern
* @return
*/
public static String dateFormat(Date date,String pattern){
return localDateTimeFormat(dateToLocalDateTime(date),pattern);
}
/**
* 将 Date 转换成LocalDateTime
* atZone()方法返回在指定时区从此Instant生成的ZonedDateTime。
* @param date
* @return
*/
public static LocalDateTime dateToLocalDateTime(Date date){
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
return instant.atZone(zoneId).toLocalDateTime();
}
/**
* 计算两个LocalDateTime 之间的毫秒数
* @param time1
* @param time2
* @return
*/
public static Long minusToMillsLocalDateTime(LocalDateTime time1,LocalDateTime time2){
return Duration.between(time1, time2).toMillis();
}
/**
* 计算两个Date之间的 Period
* @param time1
* @param time2
* @return
*/
public static Long minusToMillsDate(Date time1,Date time2){
return minusToMillsLocalDateTime(dateToLocalDateTime(time1),dateToLocalDateTime(time2));
}
/**
* 将String 按照pattern 转换成LocalDate
* @param time
* @param pattern
* @return
*/
public static LocalDate localDateParse(String time,String pattern){
return LocalDate.parse(time, DateTimeFormatter.ofPattern(pattern));
}
/**
* 将String 按照pattern 转换成LocalDateTime
* @param time
* @param pattern
* @return
*/
public static LocalDateTime localDateTimeParse(String time,String pattern){
return LocalDateTime.parse(time,DateTimeFormatter.ofPattern(pattern));
}
/**
* 计算两个Date 之间的Period
* @param date1
* @param date2
* @return
*/
public static Period periodDate(Date date1,Date date2){
return periodLocalDate(dateToLocalDate(date1),dateToLocalDate(date2));
}
/**
* 将 Date 转换成LocalDate
* atZone()方法返回在指定时区从此Instant生成的ZonedDateTime。
* @param date
* @return
*/
public static LocalDate dateToLocalDate(Date date){
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
return instant.atZone(zoneId).toLocalDate();
}
/**
* 计算两个LocalDate 之间的Period
* @param time1
* @param time2
* @return
*/
public static Period periodLocalDate(LocalDate time1,LocalDate time2){
return Period.between(time1,time2);
}
/**
* 将LocalDate 转换成 Date
* @param localDate
* @return
*/
public static Date localDateToDate(LocalDate localDate){
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = localDate.atStartOfDay(zoneId);
return Date.from(zdt.toInstant());
}
}
# 9.strictfp 关键字
strictfp的意思是FP-strict,也就是说精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算(float和double)的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令人满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。主要是为了保证相同的计算,每次都能有相同的结果,即使换个虚拟机,计算结果仍然一致,但是结果依然有可能有误差。
public strictfp class StrictTest {
public static void main(String[] args)
{
float a = 1.0f;
float b = 0.99f;
//float c = (a * 10000 - b * 10000) / 10000.f;
System.out.println(a - b); //0.00999999
}
}
strictfp interface in
{
void fun();
}
也可以看成是保证了java的可移植性。比如,double r = a*b/c这么一个计算式,是先执行a*b操作,此时会产生一个中间结果,按理说这个最多存储一个64位的double类型,但有些处理器,提供了80位的寄存器,能存储的中间结果精度更高,再拿中间结果接着做下面的计算,最终的结果截取64位,所产生的结果精度也更高。这就导致了同一个计算式,有可能在不同的处理器上,产生的结果不一致。而采用strictfp修饰的话,表示都严格按照IEEE标准,就能避免这个问题可以将一个类以及方法、接口声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字。
# 10.spring里的cron表达式0 0/10 * * *
与 0 */10 * * *区别
0 0/10 * * *
与 0 */10 * * *
的差别在于什么地方。
在说这两者的差别之前,先说下各个字符代表的含义。0
代表从0分开始,*
代表任意字符,/
代表递增。
也就是说0 0/10 * * *
代表从0
分钟开始,每10分钟执行任务一次。0 */10 * * *
代表从任务启动开始每10分钟执行任务一次。有人会问,这不是一样的么?
答案是不一样的。因为起始的时间不一样。例如:从5:07
分钟的时候执行该任务第一种写法会在5:10
的时候进行执行,写法二会在5:17
进行执行。这就是两者的差别。
当然0 0/1 * * *
与0 */1 * * *
有时会被认为是同一种写法。
?表示不指定值,使用的场景为不需要关心当前设置这个字段的值。
【秒】【分】【时】【日】【月】【周】【年】
# 11.Try-with-resources
格式:
private static void printFileJava7() throws IOException {
try(FileInputStream input = new FileInputStream("file.txt")) {
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
}
}
这就是try-with-resource 结构的用法。FileInputStream 类型变量就在try关键字后面的括号中声明。而且一个FileInputStream 类型被实例化并被赋给了这个变量。
当try语句块运行结束时,FileInputStream 会被自动关闭。这是因为FileInputStream 实现了java中的java.lang.AutoCloseable接口。所有实现了这个接口的类都可以在try-with-resources结构中使用。
当try-with-resources结构中抛出一个异常,同时FileInputStreami被关闭时(调用了其close方法)也抛出一个异常,try-with-resources结构中抛出的异常会向外传播,而FileInputStreami被关闭时抛出的异常被抑制了。这与在finally语句块中关闭资源相反。
可以在块中使用多个资源而且这些资源都能被自动地关闭。下面是例子:
上面的例子在try关键字后的括号里创建了两个资源——FileInputStream 和BufferedInputStream。当程序运行离开try语句块时,这两个资源都会被自动关闭。
这些资源将按照他们被创建顺序的逆序来关闭。首先BufferedInputStream 会被关闭,然后FileInputStream会被关闭。
这个try-with-resources结构里不仅能够操作java内置的类。你也可以在自己的类中实现java.lang.AutoCloseable接口,然后在try-with-resources结构里使用这个类。
AutoClosable 接口仅仅有一个方法,接口定义如下:任何实现了这个接口的方法都可以在try-with-resources结构中使用。
在try-with-resources中使用自定义的资源时,结束候会自动调用close方法。
# 12.YYYY和yyyy
发现YYYY是表示:当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年。
yyyy表示的单天所在的年份。
# 13. UUID
UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码,是一个128位数字的唯一标识,UUID 的目的是让分布式系统中的所有元素都能有唯一的识别信息。如此一来,每个人都可以创建不与其它人冲突的 UUID,就不需考虑数据库创建时的名称重复问题。
定义:UUID使用16进制表示,共有36个字符(32个字母数字+4个连接符"-"),格式为8-4-4-4-12
,是故 UUID 理论上的总数为16^32=2128,约等于3.4 x 10^123。也就是说若每纳秒产生1百万个 UUID,要花100亿年才会将所有 UUID 用完..
格式:UUID 的十六个八位字节被表示为 32个十六进制数字,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。例如:
6d25a684-9558-11e9-aa94-efccd7a0659b
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
数字 M
的四位表示 UUID 版本,当前规范有5个版本,M可选值为1, 2, 3, 4, 5
;UUID现有的5种版本,是根据不同的使用场景划分的,而不是根据精度,所以Version5并不会比Version1精度高,在精度上,大家都能保证唯一性,重复的概率近乎于0。
数字 N
的一至四个最高有效位表示 UUID 变体( variant ),有固定的两位10xx(1个16进制4个bit)
因此只可能取值8, 9, a, b
M中使用4位来表示UUID的版本,N中使用1-3位表示不同的variant(变体)。如上面所示:M =1, N = a表示此UUID为version-1,variant-a的UUID(Time-based ECE/RFC 4122 UUID)。
但是为什么最开始说它是一个128位的唯一标识呢?这里明明字母位数是(8+4+4+4+12)*8=256位。
因为上面的字母是用的16进制,一个16进制只代表4个bit,所以应该是(8+4+4+4+12)*4=128位。
Version1(date-time MAC address)
基于时间戳及MAC地址的UUID实现。它包括了48位的MAC地址和60位的时间戳,
接下来使用ossp-uuid命令行创建5个UUID v1。(在Mac安装brew install ossp-uuid
)
uuid -n 5 -v1
5b01c826-9561-11e9-9659-cb41250df352
5b01cc7c-9561-11e9-965a-57ad522dee7f
5b01cea2-9561-11e9-965b-a3d050dd0f99
5b01cf60-9561-11e9-965c-1b66505f58da
5b01d118-9561-11e9-965d-97354eb9e996
肉眼一看,有一种所有的UUID都很相似的感觉,几乎就要重复了!怎么回事?
其实v1为了保证唯一性,当时间精度不够时,会使用13~14位的clock sequence来扩展时间戳,比如:
当UUID的生产成速率太快,超过了系统时间的精度。时间戳的低位部分会每增加一个UUID就+1的操作来模拟更高精度的时间戳,换句话说,就是当系统时间精度无会区分2个UUID的时间先后时,为了保证唯一性,会在其中一个UUID上+1。所以UUID重复的概率几乎为0,时间戳加扩展的clock sequence一共有74bits,(2的74次方,约为1.8后面加22个零),即在每个节点下,每秒可产生1630亿不重复的UUID(因为只精确到了秒,不再是74位,所以换算了一下)。
相对于其它版本,v1还加入48位的MAC地址,这依赖于网卡供应商能提供唯一的MAC地址,同时也可能通过它反查到对应的MAC地址。Melissa病毒就是这样做到的。
Version2(date-time Mac address)
这是最神秘的版本,RFC没有提供具体的实现细节,以至于大部分的UUID库都没有实现它,只在特定的场景(DCE security)才会用到。所以绝大数情况,我们也不会碰到它。
Version3,5(namespace name-based)
V3和V5都是通过hash namespace的标识符和名称生成的。V3使用MD5作为hash函数,V5则使用SHA-1。
因为里面没有不确定的部分,所以当namespace与输入参数确定时,得到的UUID都是确定唯一的。比如:
uuid -n 3 -v3 ns:URL http://www.baidu.com
2f67490d-55a4-395e-b540-457195f7aa95
2f67490d-55a4-395e-b540-457195f7aa95
2f67490d-55a4-395e-b540-457195f7aa95
可以看到这3个UUID都是一样的。
具体的流程就是
- 把namespace和输入参数拼接在一起,如"http/http://" ++ "/query=uuid";
- 使用MD5算法对拼接后的字串进行hash,截断为128位;
- 把UUID的Version和variant字段都替换成固定的;
- 如果需要to_string,需要转为16进制和加上连接符"-"。
把V3的hash算法由MD5换成SHA-1就成了V5。
Version4(random)
这个版本使用最为广泛:
uuid -n 5 -v4
a3535b78-69dd-4a9e-9a79-57e2ea28981b
a9ba3122-d89b-4855-85f1-92a018e5c457
7c59d031-a143-4676-a8ce-1b24200ab463
533831da-eab4-4c7d-a3f6-1e2da5a4c9a0
def539e8-d298-4575-b769-b55d7637b51e
其中4位代表版本,2-3位代表variant。余下的122-121位都是全部随机的。即有2的122次方(5.3后面36个0)个UUID。一个标准实现的UUID库在生成了2.71万亿个UUID会产生重复UUID的可能性也只有50%的概率
这相当于每秒产生10亿的UUID,持续85年,而把这些UUID都存入文件,每个UUID占16bytes,总需要45 EB(exabytes),比目前最大的数据库(PB)还要大很多倍。
Summary
- 如果只是需要生成一个唯一ID,你可以使用V1或V4。
- V1基于时间戳和Mac地址,这些ID有一定的规律(你给出一个,是有可能被猜出来下一个是多少的),而且会暴露你的Mac地址。
- V4是完全随机(伪)的。 - 如果对于相同的参数需要输出相同的UUID,你可以使用V3或V5。
- V3基于MD5 hash算法,如果需要考虑与其它系统的兼容性的话,就用它,因为它出来得早,大概率大家都是用它的。
- V5基于SHA-1 hash算法,这个是首选。
java中用法:UUID.randomUUID().toString().toUpperCase()
# RunTime.getRunTime().addShutdownHook
Runtime.getRuntime().addShutdownHook(shutdownHook);
这个方法的意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁,内存数据存储等操作。
# 为什么使用token?session与token的区别
由于HTTP是一种无状态协议,服务器没有办法单单从网络连接上面知道访问者的身份,为了解决这个问题,就诞生了Cookie
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。
cookie的内容主要包括name(名字)、value(值)、maxAge(失效时间)、path(路径),domain(域)和secure
name:cookie的名字,一旦创建,名称不可更改。
value:cookie的值,如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码.
maxAge:cookie失效时间,单位秒。如果为正数,则该cookie在maxAge后失效。如果为负数,该cookie为临时cookie,关闭浏览器即失效,
浏览器也不会以任何形式保存该cookie。如果为0,表示删除该cookie。默认为-1
path:该cookie的使用路径。如果设置为"/sessionWeb/",则只有ContextPath为“/sessionWeb/”的程序可以访问该cookie。如果设置为“/”,则本域名下ContextPath都可以访问该cookie。
domain:域.可以访问该Cookie的域名。第一个字符必须为".",如果设置为".",则所有以"结尾的域名都可以访问该cookie",如果不设置,则为所有域名
secure:该cookie是否仅被使用安全协议传输。
客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,
以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
实际就是颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理
cookie 可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些 Cookie,如果 Cookie 很多,这无形地增加了客户端与服务端的数据传输量,
而 Session 的出现正是为了解决这个问题。同一个客户端每次和服务端交互时,不需要每次都传回所有的 Cookie 值,而是只要传回一个 ID,这个 ID 是客户端第一次访问服务器的时候生成的sessionID,sessionID的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串。
Session翻译过来为会话的意思,,这里的session指的是客户端与服务器之间保存状态的解决方案
一、session的状态保持及弊端
当用户第一次通过浏览器使用用户名和密码访问服务器时,服务器会验证用户数据,验证成功后在服务器端写入session数据,向客户端浏览器返回sessionid,浏览器将sessionid保存在cookie中,当用户再次访问服务器时,会携带sessionid,服务器会拿着sessionid从数据库获取session数据,然后进行用户信息查询,查询到,就会将查询到的用户信息返回,从而实现状态保持。
弊端:
1、服务器压力增大
通常session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。
2、CSRF跨站伪造请求攻击
session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
3、扩展性不强
如果将来搭建了多个服务器,虽然每个服务器都执行的是同样的业务逻辑,但是session数据是保存在内存中的(不是共享的),用户第一次访问的是服务器1,当用户再次请求时可能访问的是另外一台服务器2,服务器2获取不到session信息,就判定用户没有登陆过。
二、token认证机制
token与session的不同主要在①认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端(服务器端并不进行保存)
②浏览器会将接收到的token值存储在Local Storage中,(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
③再次访问时服务器端对token值的处理:服务器对浏览器传来的token值进行解密,解密完成后进行用户数据的查询,如果查询成功,则通过认证,实现状态保持,所以,即时有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端.
“Comparison method violates its general contract!”问题原因及解决办法
//复现代码 要求jdk7及以上
public static void main(String[] args) {
sort(1, 1, 1, 1, 1, 2, 1, 1, 1);
sort(3, 2, 3, 2, 1, 31);
sort(2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3);
// exception
sort(1, 2, 3, 2, 2, 3, 2, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
}
private static void sort(Integer... ints) {
List<Integer> list = Arrays.asList(ints);
list.sort((o1, o2) -> {
if (o1 < o2) {
return -1;
} else {
return 1;
}
});
System.out.println(list);
}
原因:在 JDK7 版本以上,Comparator 要满足自反性,传递性,对称性,不然 Arrays.sort,Collections.sort会报 IllegalArgumentException 异常。
1) 自反性:x,y 的比较结果和 y,x 的比较结果相反, 也就是说compare必须返回0,也就是compare(o1, o1) = 0。如果compare(o1,o2) = 1,则compare(o2, o1)必须返回符号相反的值也就是 -1
2) 传递性:x>y,y>z,则 x>z。
3) 对称性:x=y,则 x,z 比较结果和 y,z 比较结果相同。(也叫可逆比较)
单例模式简介
单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点。下面就来讲讲Java中的N种实现单例模式的写法。
饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
这是实现一个安全的单例模式的最简单粗暴的写法,这种实现方式我们称之为饿汉式。之所以称之为饿汉式,是因为肚子很饿了,想马上吃到东西,不想等待生产时间。这种写法,在类被加载的时候就把Singleton实例给创建出来了。
饿汉式的缺点就是,可能在还不需要此实例的时候就已经把实例创建出来了,没起到lazy loading的效果。优点就是实现简单,而且安全可靠。
懒汉式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
相比饿汉式,懒汉式显得没那么“饿”,在真正需要的时候再去创建实例。在getInstance方法中,先判断实例是否为空再决定是否去创建实例,看起来似乎很完美,但是存在线程安全问题。在并发获取实例的时候,可能会存在构建了多个实例的情况。所以,需要对此代码进行下改进。
public class SingletonSafe {
private static volatile SingletonSafe singleton;
private SingletonSafe() {
}
public static SingletonSafe getSingleton() {
if (singleton == null) {
synchronized (SingletonSafe.class) {
if (singleton == null) {
singleton = new SingletonSafe();
}
}
}
return singleton;
}
}
这里采用了双重校验的方式,对懒汉式单例模式做了线程安全处理。通过加锁,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。
静态内部类
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。
除了反射攻击之外,还可能存在反序列化攻击的情况。如下:
引入依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
这个依赖提供了序列化和反序列化工具类。
Singleton类实现java.io.Serializable接口。
如下:
public class Singleton implements Serializable {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance);
}
}
在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
通过枚举实现单例模式
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
调用方法:
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。