单例模式、一致性哈希、redis防止数据丢失
单例模式
被问了一个问题,怎么验证单例模式。想半天没想出来,记录一下。
不考虑多线程
public class Singleton {
private static Singleton singleton;
// 将构造函数私有化,只能通过getInstance访问实例
private Singleton() {
}
public static Singleton getInstance() {
if(singleton==null) {
singleton = new Singleton();
}
return singleton;
}
}
测试代码
private static Singleton singleton;
public static Singleton getInstance() throws InterruptedException {
if(singleton==null) {
// 为了方便验证,手动加一个随机延迟
Thread.sleep(new Random().nextInt(500));
singleton = new Singleton();
}
return singleton;
}
public static void testSingleton() throws InterruptedException {
// =======1======见下文
final CountDownLatch latch = new CountDownLatch(1000);
final ConcurrentHashMap<Singleton,Integer> map = new ConcurrentHashMap<>();
for(int i=0;i<1000;i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton s = null;
try {
s = Singleton.getInstance();
// ========2=======见下文
synchronized (s) {
map.put(s,map.getOrDefault(s,0)+1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
}
}).start();
}
latch.await();
for(Singleton s:map.keySet()) {
System.out.println(s+":"+map.get(s));
}
System.out.println("done");
}
public static void main(String[] args) throws InterruptedException {
Singleton.testSingleton();
}
====2=====
处用final
final变量,如果是基本类型,数值在初始化之后不能更改,如果是引用类型,在初始化之后便不能指向另一个对象。
====2=====
处原本的想法是:
map.put(s,map.getOrDefault(s,0)+1);
但是这种实际是两个操作,就算用DCL,结果也不是1000
leetcode.Singleton@2464496:904
done
可以加个对象锁,s有冲突也肯定是同一个实例对象
synchronized (s) {
map.put(s,map.getOrDefault(s,0)+1);
}
结果是:
leetcode.Singleton@5a344135:1
...
leetcode.Singleton@68ee5860:303
leetcode.Singleton@7449d4b:1
leetcode.Singleton@660e745c:3
leetcode.Singleton@7bc6d1d0:1
...
leetcode.Singleton@7995ef7f:1
leetcode.Singleton@2bef47c3:210
leetcode.Singleton@3a68c2d8:107
leetcode.Singleton@22f26843:1
leetcode.Singleton@6e311d43:1
...
leetcode.Singleton@65666f5a:1
leetcode.Singleton@2ea64cd8:130
done
很明显,不是线程安全的。
双重校验锁
加延时是为了测试并发情况,模拟被打断。
private static Singleton singleton;
public static Singleton getInstance() throws InterruptedException {
if(singleton==null) {
//Thread.sleep(new Random().nextInt(500));
synchronized (Singleton.class) {
if(singleton==null) {
//Thread.sleep(new Random().nextInt(500));
singleton = new Singleton(); //1
}
}
}
return singleton;
}
结果是:
leetcode.Singleton@5caf9db6:1000
done
隐患
1处代码,顺序是:
- 分配内存(有房了)
- 初始化(装修了)
- 将对象指向刚分配的空间(住进去了)
但是有些编译器为了性能,会进行重排序,也就是:
- 分配内存(有房了)
- 将对象指向刚分配的空间(住进去了)
- 初始化(装修了)
这种重排序情况,如果线程A到了对象指向内存空间
阶段,但还没初始化,另一个线程B看到singleton
不为null,直接返回了,这时就会出现问题。所以要声明为volatile
,禁止指令重排序。
双重校验锁改进
private static volatile Singleton singleton;
public static Singleton getInstance() throws InterruptedException {
if(singleton==null) {
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
静态内部类
// 静态内部类单例模式
public class Singleton {
private Singleton() {}
private static class InstanceHolder {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.instance;
}
// 测试效果
// interview1.Singleton@4a1a7268:1000
// done
}
外部类加载的时候不会立即加载内部类。
类的加载时机(如果未加载):
- new或者读取设置静态字段,调用静态方法时。
- 反射调用时
- 初始化一个类时,如果父类未加载,会先加载父类。
- 虚拟机启动时,指定main()方法的主类、
- java.lang.invoke.MethodHandle实例最后的解析结果的方法句柄(不了解)
在上面例子中,只有getInstance方法被调用时,InstanceHandler才在Singleton的运行时常量池里,把符号引用替换为直接引用,INSTANCE才被真正创建。
INSTANCE创建过程是线程安全的。
缺点就是:以静态内部类形式创建单例,无法传参。
破坏单例模式
public static void testSingletonReflect() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1000);
ConcurrentHashMap<Singleton, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Constructor<Singleton> constructor = null;
try {
//constructor = Singleton.class.getConstructor();
// getConstructor()得不到类的私有构造器
// getDecl...可以,但是要setAccessible(true)才能获取
constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s = constructor.newInstance();
synchronized (s) {
map.put(s,map.getOrDefault(s,0)+1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
latch.countDown();
}
latch.await();
for(Singleton s:map.keySet()) {
System.out.println(s+":"+map.get(s));
}
System.out.println("testSingletonReflect() done");
}
j结果:
interview1.Singleton@6a8a5e8b:1
interview1.Singleton@53925f91:1
interview1.Singleton@6519891a:1
...
这些方法的单例模式可以被反射破坏。
对比
- 饿汉式:初始化的时候就加载了,不能实现懒加载,造成内存空间浪费。
- 懒汉式:可以DCL加锁解决。
- 静态内部类:传参问题。
==枚举法==
enum Type {
A,B,C,D;
static int value = 3;
public static int getValue() {
return value;
}
}
可以看做一个类,ABCD是它的实例,enum的构造方法是私有的。
通过反射要绕过的话会报错:
java.lang.NoSuchMethodException: interview1.SingletonDemo.<init>()
反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
enum SingletonDemo {
INSTANCE;
public static SingletonDemo getInstance() {
return INSTANCE;
}
private String name;
public void sayHi(String n) {
name = n;
System.out.println("hi "+name);
}
}
序列化问题
序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
枚举在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
一致性哈希
增加虚拟结点,这样宕机或者加机器的时候,移动的结点也更能分散到其他的机器上。
Redis防止数据丢失
RDB
RDB:数据快照。通过执行save
(前台阻塞)或者bgsave
(后台)生成本地快照,是压缩写入的,所以体积要比实例内存小。加载的时候恢复也相对快些。问题:数据补全(某一时刻的快照)、代价较大,消耗大量CPU和内存资源。
后台保存的时候回生成一个子进程,扫描保存内存数据。写时拷贝技术。没有改动的照常,共享内存地址空间,有改动的使用新的内存地址。通过这样写时拷贝技术减少内存复制。
AOF
AOF:实时追加命令的日志文件。
文件刷盘时机:
- 每次写入都刷,性能影响最大,磁盘IO,数据安全性最高。
- 1s一刷,对性能影响较小,结点宕机最多丢失一秒的数据。
- 按照操作系统的机制来刷盘。
有AOF重写机制,因为指令越多越来越来多,文件越来越大,通过扫描整个实例的数据,重新生成一个AOF文件。
总结
通常可以选择每秒刷盘的机制,既能保证良好的写入性能,在实例宕机时最多丢失一秒的数据,做到性能和安全的平衡。
顺便:redis分布式锁问题
setnx key value
。要设置过期时间,否则可能获取锁的线程崩溃,永远锁住。
设置登录密码:/etc/redis.conf下面
# requirepass foobared
取消注释,改成自己设置的密码vim 右键visual问题:set mouse-=a
hqinglau@centos:~$ redis-cli
127.0.0.1:6379> setnx lock Java # set if not exist
(integer) 1
127.0.0.1:6379> setnx lock C++ # 已经有值了,设置失败
(integer) 0
127.0.0.1:6379> ttl lock #查询过期时间
(integer) -1
设置过期时间:
127.0.0.1:6379> expire lock 10
(integer) 1
127.0.0.1:6379> ttl lock
(integer) 7
127.0.0.1:6379> ttl lock
(integer) 6
127.0.0.1:6379> ttl lock
(integer) 1
127.0.0.1:6379> ttl lock
(integer) -2
127.0.0.1:6379> ttl lock
(integer) -2
127.0.0.1:6379> get lock
(nil)
但是这样就是两个步骤了,不是原子操作了,可以一步完成。setex,会覆写旧值。
127.0.0.1:6379> setex lock 10 java
OK
127.0.0.1:6379> get lock
"java"
127.0.0.1:6379> #10s later
(error) ERR unknown command '#10s'
127.0.0.1:6379> get lock
(nil)
兼容操作:
127.0.0.1:6379> set lock java nx ex 10
OK
127.0.0.1:6379> get lock
"java"
127.0.0.1:6379> get lock
(nil)
redis分布式锁释放:
首先不能直接释放,可能不是自己加的锁;也不能判断value然后释放,因为判断和释放不是原子操作,中间可能被打断。可以用手动封装一个锁把它变成原子操作。