共计 2997 个字符,预计需要花费 8 分钟才能阅读完成。
简介
如果咱们在多线程中引入了共享变量,那么咱们就须要考虑一下多线程下线程平安的问题了。那么咱们在编写代码的过程中,须要留神哪些线程平安的问题呢?
一起来看看吧。
留神线程平安办法的重写
大家都做过办法重写,咱们晓得办法重写是不会查看办法修饰符的,也就是说,咱们能够将一个 synchronized 的办法重写成为非线程平安的办法:
public class SafeA {public synchronized void doSomething(){}}
public class UnsafeB extends SafeA{
@Override
public void doSomething(){}
}
咱们在实现子类性能的时候,肯定要放弃办法的线程安全性。
构造函数中 this 的溢出
this 是什么呢?依据 JLS 的标准,当用作次要表达式时,关键字 this 示意一个值,该值是对其调用实例办法的对象或正在结构的对象的援用。
那么问题来了,因为 this 可能示意正在结构的对象,那么意味着,如果对象还没有构建结束,而 this 又能够被内部拜访的话,就会造成内部对象拜访到还未结构胜利对象的问题。
咱们来具体看一下 this 溢出都会产生在哪些状况:
public class ChildUnsafe1 {
public static ChildUnsafe1 childUnsafe1;
int age;
ChildUnsafe1(int age){
childUnsafe1 = this;
this.age = age;
}
}
下面是一个非常简单的 this 溢出的状况,在构造函数的过程中,将 this 赋值给了一个 public 对象,将会导致 this 还没有被初始化结束就被其余对象拜访。
那么咱们调整一下程序是不是就能够了呢?
public class ChildUnsafe2 {
public static ChildUnsafe2 childUnsafe2;
int age;
ChildUnsafe2(int age){
this.age = age;
childUnsafe2 = this;
}
}
下面咱们看到,this 的赋值被放到了构造方法的最初面,是不是就能够防止拜访到未初始化结束的对象呢?
答案是否定的,因为 java 会对代码进行重排序,所以 childUnsafe2 = this 的地位是不定的。
咱们须要这样批改:
public class Childsafe2 {
public volatile static Childsafe2 childUnsafe2;
int age;
Childsafe2(int age){
this.age = age;
childUnsafe2 = this;
}
}
加一个 volatile 描述符,禁止重排序,完满解决。
咱们再来看一个父子类的问题,还是下面的 Childsafe2,咱们再为它写一个子类:
public class ChildUnsafe3 extends Childsafe2{
private Object obj;
ChildUnsafe3(int age){super(10);
obj= new Object();}
public void doSomething(){System.out.println(obj.toString());
}
}
下面的例子有什么问题呢?因为父类在调用构造函数的时候,曾经裸露了 this 变量,所以可能会导致 ChildUnsafe3 中的 obj 还没有被初始化的时候,内部程序就调用了 doSomething(),这个时候 obj 还没有被初始化,所以会抛出 NullPointerException。
解决办法就是不要在构造函数中设置 this,咱们能够新创建一个办法,在结构函数调用结束之后,再进行设置。
不要在类初始化的时候应用后盾线程
如果在类初始化的过程中,应用后盾过程,有可能会造成死锁,咱们思考上面的状况:
public final class ChildFactory {
private static int age;
static {Thread ageInitializerThread = new Thread(()->{System.out.println("in thread running");
age=10;
});
ageInitializerThread.start();
try {ageInitializerThread.join();
} catch (InterruptedException ie) {throw new AssertionError(ie);
}
}
public static int getAge() {if (age == 0) {throw new IllegalStateException("Error initializing age");
}
return age;
}
public static void main(String[] args) {int age = getAge();
}
}
下面的类应用了一个 static 的 block,在这个 block 中,咱们启动一个后盾过程来设置 age 这个字段。
为了保障可见性,static 变量必须在其余线程运行之前初始化结束,所以 ageInitializerThread 须要期待 main 线程的 static 变量执行结束之后能力运行,然而咱们又调用了 ageInitializerThread.join() 办法,主线程又须要反过来期待 ageInitializerThread 的执行结束。
最终导致了循环期待,造成了死锁。
最简略的解决办法就是不应用后盾过程,间接在 static block 中设置:
public final class ChildFactory2 {
private static int age;
static {System.out.println("in thread running");
age=10;
}
public static int getAge() {if (age == 0) {throw new IllegalStateException("Error initializing age");
}
return age;
}
public static void main(String[] args) {int age = getAge();
}
}
还有一种方法就是应用 ThreadLocal 将初始化变量保留在线程本地。
public final class ChildFactory3 {private static final ThreadLocal<Integer> ageHolder = ThreadLocal.withInitial(() -> 10);
public static int getAge() {int localAge = ageHolder.get();
if (localAge == 0) {throw new IllegalStateException("Error initializing age");
}
return localAge;
}
public static void main(String[] args) {int age = getAge();
}
}
本文的代码:
learn-java-base-9-to-20/tree/master/security
本文已收录于 http://www.flydean.com/java-security-code-line-threadsafe/
最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!