簡介
在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/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!