由于String会被存储到常量池中,我们知道,一般不会使用String来作为同步锁,从两方面考虑
-
我们用String作为锁,并希望它能像Object一样,不同变量加锁互不影响。然而,有时2个String对象可能指向常量池中同一个字符串,导致其加锁互相影响。一个例子如下,若在2个类中,使用了字符串字面量赋值的方式声明2个String对象,并用
synchronized
关键字对两个String对象分别加锁,由于字符串常量池,2个String对象中的字符串对象指向的是常量池中同一个内存区域,故2个加锁方法会彼此影响有2个类StringSyn1和StringSyn2,它们各自有个lock方法
package com.yogurt.test; import java.util.concurrent.TimeUnit; public class StringSyn1 { private String lockString = "yogurt"; public void lock() { synchronized (lockString) { System.out.println("StringSyn1 get lock"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("StringSyn1 is about to exit"); } } }
package com.yogurt.test; import java.util.concurrent.TimeUnit; public class StringSyn2 { private String lockString = "yogurt"; public void lock() { synchronized (lockString) { System.out.println("StringSyn2 get lock"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("StringSyn2 is about to exit"); } } }
再创建一个测试类,运行
public static void main(String[] args) { StringSyn1 syn1 = new StringSyn1(); StringSyn2 syn2 = new StringSyn2(); (new Thread(() -> syn1.lock())).start(); (new Thread(() -> syn2.lock())).start(); }
可以看到syn1和syn2依次打印,因为2个类的lockString变量是通过字符串字面量进行赋值,这样的方式使得2个类的lockString变量都指向了常量池中的同一个字符串对象,2个类的lock方法在同一个String对象上进行了同步,故syn2被syn1阻塞。**在实际开发中,我们很难知道用作锁的String对象,是否来自于常量池,是否有其他地方也将其用作锁。**当然,如果将上方lockString的部分改为
private String lockString = new String("yogurt");
这样2个类中的lockString则分别是2个不同的String对象,则可以看到2个类的lock方法互不影响,syn2没有等到syn1结束后才执行 -
当String具有业务含义,我们希望,相同内容的字符串,加的是同一把锁。比如使用订单号来加锁,我们希望某一个订单号,同一时刻只能有一个针对该订单号的操作。那么只要订单号相同,加的应是一把锁。当有一个订单号正在被处理时,多个相同订单号的请求进来后,都应该在这把锁上排队等待。这是我们期望的,然而,字符串内容相同,也不能保证是同一个String对象。如上面例子所示,若2个字符串对象都引用自常量池,则它们是同一个对象,可以达到相同内容字符串加同一把锁的效果。但如果2个字符串对象具有相同内容,但却是2个不同的对象呢?比如上面使用new关键字来创建字符串。假设有2个请求,同时到来,它们携带的订单号相同,但是存储订单号的字符串对象是不同的对象,此时加锁就会失败,会导致对同一个订单号同时进行了临界操作。
一般只有字符串具有业务含义时,才会需要使用String作为锁,即第二种情况才需要用String做锁。而第一种情况用普通的Object作为锁即可。
那么有什么解决方法呢?最简单的就是利用字符串常量池的特性,调用一下String的intern方法,将String对象放入常量池,再用其做锁,这样就能保证相同内容的字符串变量,指向的是同一个对象了。
但是java原生的intern,不会自动清理常量池,可能会导致频繁的GC。
好在Google的guava包中有一个Interner,可以用其创建基于弱引用的常量池,避免GC问题。使用例子如下
package com.yogurt.test;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
public class StringSyn {
private Interner<String> stringPool = Interners.newWeakInterner();
/**
* 针对具体订单号,执行临界操作
* **/
public void doCriticalOperation(String orderId) {
synchronized (stringPool.intern(orderId)) {
//do something
}
}
}