关于java:java安全编码指南之lock和同步的正确使用

简介

在java多线程环境中,lock和同步是咱们肯定会应用到的性能。那么在java中编写lock和同步相干的代码之后,须要留神哪些问题呢?一起来看看吧。

应用private final object来作为lock对象

一般来说咱们在做多线程共享对象的时候就须要进行同步。java中有两种同步形式,第一种就是办法同步,第二种是同步块。

如果咱们在实例办法中应用的是synchronized关键字,或者在同步块中应用的是synchronized(this),那么会以该该对象的实例作为monitor,咱们称之为intrinsic lock。

如果有恶意代码歹意获取该对象的锁并且开释,那么咱们的零碎将不能及时响应失常的服务,将会蒙受到DOS攻打。

解决这种问题的办法就是应用private final object来作为lock的对象。因为是private的,所以歹意对象无奈获取到该对象的锁,从而防止了问题的产生。

如果是在类办法(static)中应用了synchronized关键字,那么将会以这个class对象作为monitor。这种状况下,歹意对象能够通过该class的子类或者间接获取到该class,而后通过调用getClass()获取到class对象,从而进行加锁操作,让失常服务无奈获取到锁。

所以,咱们举荐应用private final object来作为lock对象。

上面举几个例子来阐明:

public class SynObject {

    public synchronized  void doSomething(){
        //do something
    }

    public static void main(String[] args) throws InterruptedException {
        SynObject synObject= new SynObject();
        synchronized (synObject){
            while (true){
                //loop forever
                Thread.sleep(10000);
            }
        }
    }
}

下面代码可能使咱们最常应用的代码,咱们在对象中定义了一个synchronized的doSomething办法。

如果有恶意代码间接拿到了咱们要调用的SynObject对象,并且间接对其进行同步,如上例所示,那么这个对象的锁将永远无奈开释。最终导致DOS。

咱们看第二种写法:

    public Object lock = new Object();

    public void doSomething2(){
        synchronized (lock){
            //do something
        }
    }

下面的例子中,咱们同步了一个public对象,然而因为该对象是public的,所以恶意程序齐全能够拜访该public字段,并且永恒取得这个对象的monitor,从而产生DOS。

再看上面的一个例子:

    private volatile Object lock2 = new Object();

    public void doSomething3() {
        synchronized (lock2) {
            // do something
        }
    }

    public void setLock2(Object lockValue) {
        lock2 = lockValue;
    }

下面的例子中,咱们定义了一个private的lock对象,并且应用它来为doSomething3办法加锁。

尽管是private的,然而咱们提供了一个public的办法来对该对象进行批改。所以也是有平安问题的。

正确的做法是应用private final Object:

    private final Object lock4= new Object();

    public void doSomething4() {
        synchronized (lock4) {
            // do something
        }
    }

咱们再考虑一下静态方法的状况:

    public static synchronized void doSomething5() {
        // do something
    }

synchronized (SynObject.class) {
  while (true) {
    Thread.sleep(10000); 
  }
}

下面定义了一个public static的办法,从而锁定的是class对象,恶意代码能够歹意占有该对象的锁,从而导致DOS。

不要synchronize可被重用的对象

之前咱们在讲表达式规定的时候,提到了封装类对象的构建准则:

对于Boolean和Byte来说,如果间接从根底类值构建的话,也是同一个对象。

而对于Character来说,如果值的范畴在u0000 to u007f,则属于同一个对象,如果超出了这个范畴,则是不同的对象。

对于Integer和Short来说,如果值的范畴在-128 and 127,则属于同一个对象,如果超出了这个范畴,则是不同的对象。

举个例子:

        Boolean boolA=true;
        Boolean boolB=true;
        System.out.println(boolA==boolB);

下面从根底类型构建的Boolean对象其实是同一个对象。

如果咱们在代码中应用上面的Boolean对象来进行同步,则可能会触发平安问题:

private final Boolean booleanLock = Boolean.FALSE;
 
public void doSomething() {
  synchronized (booleanLock) {
    // ...
  }
}

下面的例子中,咱们从Boolean.FALSE构建了一个Boolean对象,尽管这个对象是private的,然而恶意代码能够通过Boolean.FALSE来构建一个雷同的对象,从而让private规定生效。

同样的问题也可能呈现在String中:

private final String lock = "lock";
 
public void doSomething() {
  synchronized (lock) {
    // ...
  }
}

因为String对象有字符串常量池,间接通过字符串来创立的String对象其实是同一个对象。所以下面的代码是有平安问题的。

解决办法就是应用new来新创建一个对象。

private final String lock = new String("LOCK");

不要sync Object.getClass()

有时候咱们想要同步class类,Object提供了一个不便的getClass办法来返回以后的类。然而如果在父类和子类的状况下,子类的getClass会返回子类的class类而不是父类的class类,从而产生不统一对象同步的状况。

看上面的一个例子:

public class SycClass {

    public void doSomething(){
        synchronized (getClass()){
            //do something
        }
    }
}

在SycClass中,咱们定义了一个doSomething办法,在该办法中,咱们sync的是getClass()返回的对象。

如果SycClass有子类的状况下:

public class SycClassSub extends SycClass{

    public void doSomethingElse(){
        synchronized (SycClass.class){
           doSomething();
        }
    }
}

doSomethingElse办法实际上取得了两个锁,一个是SycClass,一个是SycClassSub,从而产生了安全隐患。

在sync的时候,咱们须要明确指定要同步的对象,有两种办法指定要同步的class:

synchronized (SycClass.class)
synchronized (Class.forName("com.flydean.SycClass"))

咱们能够间接调用SycClass.class也能够应用Class.forName来获取。

不要sync高级并发对象

咱们把实现了java.util.concurrent.locks包中的Lock和Condition接口的对象称作高级并发对象。比方:ReentrantLock。

这些高级并发对象看起来也是一个个的Lock,那么咱们可不可以间接sync这些高级并发对象呢?看上面的例子:

public class SyncLock {

    private final Lock lock = new ReentrantLock();

    public void doSomething(){
        synchronized (lock){
            //do something
        }
    }
}

看起来如同没问题,然而咱们要留神的是,咱们自定义的synchronized (lock)和高级并发对象中的Lock实现是不一样的,如果咱们同时应用了synchronized (lock)和Lock自带的lock.lock(),那么就有可能产生安全隐患。

所以,对于这些高级并发对象,最好的做法就是不要间接sync,而是应用他们自带的lock机制,如下:

    public void doSomething2(){
        lock.lock();
        try{
        //do something
        }finally {
            lock.unlock();
        }
    }

不要应用Instance lock来爱护static数据

一个class中能够有static类变量,也能够有实例变量。类变量是和class相干的,而实例变量是和class的实例对象相干的。

那么咱们在爱护类变量的时候,肯定要留神sync的也必须是类变量,如果sync的是实例变量,就无奈达到爱护的目标。

看上面的一个例子:

public class SyncStatic {
    private static volatile int age;

    public synchronized void doSomething(){
        age++;
    }
}

咱们定义了一个static变量age,而后在一个办法中心愿对其累加。之前的文章咱们也讲过了,++是一个复合操作,咱们须要对其进行数据同步。

然而下面的例子中,咱们应用了synchronized关键字,同步的实际上是SyncStatic的实例对象,如果有多个线程创立多个实例对象同时调用doSomething办法,齐全是能够并行进行的。从而导致++操作呈现问题。

同样的,上面的代码也是一样的问题:

    private final Object lock = new Object();
    public  void doSomething2(){
        synchronized (lock) {
            age++;
        }
    }

解决办法就是定义一个类变量:

    private static final Object lock3 = new Object();
    public  void doSomething3(){
        synchronized (lock3) {
            age++;
        }
    }

在持有lock期间,不要做耗时操作

如果在持有lock期间,咱们进行了比拟耗时的操作,像I/O操作,那么持有lock的工夫就会过长,如果是在高并发的状况下,就有可能呈现线程饿死的状况,或者DOS。

所以这种状况咱们肯定要防止。

正确开释锁

在持有锁之后,肯定要留神正确的开释锁,即便遇到了异样也不应该打断锁的开释。

一般来说锁放在finally{}中开释最好。

    public void doSomething(){
        lock.lock();
        try{
        //do something
        }finally {
            lock.unlock();
        }
    }

本文的代码:

learn-java-base-9-to-20/tree/master/security

本文已收录于 http://www.flydean.com/java-security-code-line-lock/

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

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

评论

发表回复

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

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