目录
1.发布对象
发布对象:使一个对象能够被当前范围之外的代码所使用
对象溢出:一种错误的发布,当一个对象还没有构造完成就使他被其他线程所见。
2.不安全的发布对象
下面给出一个不安全发布对象的例子:
public class Test4 {
private int[] ints = {1, 2, 3};
public int[] getInts() {
return ints;
}
public static void main(String[] args) {
Test4 test4 = new Test4();
System.out.println(Arrays.toString(test4.getInts()));
test4.getInts()[1] = 555;
System.out.println(Arrays.toString(test4.getInts()));
}
}
这里是通过public修饰的get方法导致了对象的溢出,或者说不安全,因为他对象的私有域可能会被篡改。导致其他线程拿到不正确值。
再举一个例子:
public class Test5 {
private int a = 1;
public Test5() {
new InnerClass();
}
class InnerClass {
public InnerClass() {
System.out.println(Test5.this.a);
}
}
public static void main(String[] args) {
new Test5();
}
}
这里是对象还没有构造完成就通过Test5.this给发布了出去,所以也是不安全的。
3.四种安全发布对象的方法
- 1.在静态初始化函数中初始化一个对象
这个最常见的就是单例模式了,饱汉模式->饿汉模式->饿汉模式的静态方法加锁->双检查锁机制->volatile修饰
下面说说每个的特点
饱汉模式不是线程安全
public class SingletonExample1 {
private SingletonExample1() {
}
private static SingletonExample1 instance = null;
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
饿汉模式可能会浪费资源
public class SingletonExample1 {
private SingletonExample1() {
}
private static SingletonExample1 instance = new SingletonExample1();
public static SingletonExample1 getInstance() {
return instance;
}
}
说道饿汉模式再多说一句,如果对象放在静态代码块中去初始化会出现什么情况呢?如果顺序不当会导致返回的结果为空,静态域的执行是讲顺序的。
- 2.将对象应用保存到一个由锁保护的域中
饱汉模式的静态方法加锁会导致阻塞
public class SingletonExample1 {
private SingletonExample1() {
}
private static SingletonExample1 instance = null;
public static synchronized SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
双检查锁机制,CPU指令重排序导致出现多个实例
public class SingletonExample1 {
private SingletonExample1() {
}
private static SingletonExample1 instance = null;
public static SingletonExample1 getInstance() {
if (instance == null) {
synchronized (SingletonExample1.class) {
if (instance == null) {
instance = new SingletonExample1();
}
}
}
return instance;
}
}
- 3.将对象的引用保存到volatile类型域或者AtomicReference对象中
这里做一个说明:instance = new SingletonExample1();比如这行代码在会有三个动作,分配内存空间,初始化对象,instance 指向刚分配的对象。CPU指令重排后,二步和三步颠倒就会出现两个实例。
那么防止CPU指令重排不就安全了吗?对的,使用volatile就可以防止指令重排
public class SingletonExample1 {
private SingletonExample1() {
}
private static volatile SingletonExample1 instance = null;
public static SingletonExample1 getInstance() {
if (instance == null) {
synchronized (SingletonExample1.class) {
if (instance == null) {
instance = new SingletonExample1();
}
}
}
return instance;
}
}
ok,介绍完了饱汉饿汉的单例写法,那么还有没有其他的写法呢?使用枚举来做实例化其实是最安全的。使用JVM来保证只有一个对象,使用这种方式相比于懒汉饿汉模式都有优点。
public class SingletonExample1 {
private SingletonExample1() {
}
public static SingletonExample1 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample1 instance;
//用JVM来保证方法绝对只被调用一次
Singleton() {
instance = new SingletonExample1();
}
public SingletonExample1 getInstance() {
return instance;
}
}
}
- 4.将对象的引用保存到某个正确构造对象的final类型域中
4.不可变对象
有一种对象只要发布了就是安全的,那就是不可变对象,比如String类,
final关键字,修饰类,修饰方法,变量(基本类型,引用类型,方法参数)
java中除了final可以定义不可变的对象是否还有比其他的手段定义不可变对象?
final修饰的Map引用虽然不可以指向其他引用,但是值还可以修改,如果使用Collections.unmodifiableMap()那么他连值都不能修改。
还有一种方式就是使用谷歌Guava的Immutable,
做成不可变对象的好处就是多线程环境下是线程安全的。
5.线程封闭
使用不可变对象史为了躲开并发,还有一种躲开并发的操作就是线程封闭。就是把一个对象封装到一个线程里面,只有这一个线程能访问。
主要掌握ThreadLocal的用法
6.线程不安全类的写法
StringBilder(不安全) StringBuffer(安全)
StringBuffer的线程安全的实现是通过synchronized加锁实现的,所以通常我们使用StringBilder,让她堆栈封闭就可以了。
SimpleDateFormat(不安全) DateTimeFormatter(安全)注意这不是time包下的,而是JodaTime包下的。
集合(集合我们这里暂时不讲,主要在java基础里面去讲)
一种常见的线程不安全的写法:先检查,再执行,这样的话是分成了两个操作,即使之前的操作安全后面可能回出现问题。所以这个a对象要仔细想想看,他是不是多线程共享的,如果是多线程共享的,一定要加一个锁。