单例是最为常见的一种设计模式,关于单例也有非常多种实现方式,平时看到最多的就是饿汉式和懒汉式,我在前面的设计模式章节有详解,这里不多说,我这里仅提一下一种基于饿汉式的改进单例,具有诸多优点,在spring源码中亦有大量使用.是采用枚举的方式实现的单例,好处显而易见,我们在传统的饿汉式实现方式中,虽然解决了线程安全问题,但是饿汉式不能做到在调用时才创建对象,有时对象已经被创建了,但却没被使用,会造成资源浪费,下面贴一下基于枚举的单例实现:
//枚举式单例
public class SingletonTest {
private SingletonTest(){
}
private SingletonTest singletonTest;
public static SingletonTest getInstence(){
return Singleton.INSTENCE.getSingletonTest();
}
private enum Singleton{
INSTENCE;
private SingletonTest singletonTest;
Singleton(){
singletonTest = new SingletonTest();
}
public SingletonTest getSingletonTest(){
return singletonTest;
}
}
}
测试一下:
没有问题.
不可变对象是指该对象一旦被初始化创建之后就不能对其状态进行改变了,常见的比如String,Integer等包装类.
对于集合,可以使用Collections.unmodifiableCollection()方法将集合转为不可变集合,这样可以使多线程下对集合的操作更安全.
线程封闭有好几种方式,比较常见的有
堆栈封闭:比如局部变量,就不会造成并发问题,局部变量是放在栈中的,每个线程私有.
ThreadLocal线程封闭,比较推荐的一种方式,ThreadLocal底层使用map存放当前线程和所要存储的值,当前线程作为Key,被存储的数据作为value.
这样就可以保证每个线程私有被存储的变量,不会被其它线程看到.
顺便提一下InheritableThreadLocal,ThreadLocal中存储的值不能被继承该线程类的子类所使用,但InheritableThreadLocal可以.
常见的线程不安全类和对应的线程安全类:
StringBuilder --- StringBuffer
SimpleDateFarmat --- JodaTime/JDK8提供的Time库
ArrayList --- CopyOnWriteArrayList
...
线程不安全的写法:
先检查,后执行.比如我们在单例中的懒汉式
if(condition){//todo}
常见的同步容器有: vector,stack,hashtable等,在大多数情况下,推荐使用JUC包提供的并发容器,性能更高.
也可以使用Collections.synchronizedXXX来将对应的容器变为并发容器.
值得一提的是,所有这些并发容器,并不是完全线程安全的,添加/移除单个元素是没问题的,但涉及addAll,containsAll等多元素操作时,需要额外加锁,否则无法保证线程安全.
ArrayList -> CopyOnWriteArrayList
CopyOnWriteArrayList仅对set/add等改变数组内容的方法加了锁,对get是无锁的
TreeMap -> ConcurrentSkipListMap
HashMap -> ConcurrentHashMap
HashSet -> CopyOnWriteArraySet
TreeSet -> CopyOnWriteSkipListSet
其中HashMap,ConcurrentHashMap在JDK1.7和JDK1.8中的源码都建议阅读,我在面试题章节中有梳理,这里就不再赘述了.
将以上几种策略总结如下:
1.线程限制:一个由线程限制的对象,由线程独占,并且只能被占有它的线程修改.
2.共享只读:一个被共享只读的对象,在没有额外同步的情况下,可以被多个线程并发同步访问,但任何线程都不能修改它.
3.线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外同步就可以通过公共接口随意访问它.
4.被守护对象:被守护对象只能通过获取特定的锁来访问.
以上便是高并发场景下,常见的线程安全策略.