关于java:java安全编码指南之线程安全规则

简介

如果咱们在多线程中引入了共享变量,那么咱们就须要考虑一下多线程下线程平安的问题了。那么咱们在编写代码的过程中,须要留神哪些线程平安的问题呢?

一起来看看吧。

留神线程平安办法的重写

大家都做过办法重写,咱们晓得办法重写是不会查看办法修饰符的,也就是说,咱们能够将一个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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」,懂技术,更懂你!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理