解决方案 »
- java 对静态初始化的疑问!!!
- 想用swing组件模拟英文键盘,求布局思路
- 接口、继承;什么时候使用接口?什么时候使用继承?为什么要使用接口?为什么要使用继承?
- 【300分求解】如何弹出一个小窗口,显示一个图片并输入图片上的文字,然后返回输入的内容
- 菜鸟问题,如何得到vector中元素得个数??
- 一个棘手的问题,请高手帮忙!!
- 数据库连接出现了超奇怪的问题!!!!
- 前辈谈一下J2SE J2EE J2ME区别
- 如何给HttpSession加上HttpSessionListener?(在线加分)
- list元素合并的问题
- 开始学习JAVA啦,安卓也在了解中,散分了,来者有分
- 为什么我的JAVA SOCKET客户端不在本机就连接不上啊
public class Test { private final AtomicInteger[] iAccounts;
public Test(int count, int money) {
iAccounts = new AtomicInteger[count];
for (int i = 0; i < count; i++)
iAccounts[i] = new AtomicInteger(money);
} void transferAtomic(int from, int to, int money) {
if (iAccounts[from].get() < money)
return;
iAccounts[from].getAndAdd(-money);
try {
Thread.sleep(2);
}
catch (Exception e) {
}
iAccounts[to].getAndAdd(money);
}
//...以下略
新建一个TransferRunner,用transferAtomic而不用transfer函数。此方法比用KeyLock快了10倍。测试的时候去掉那句sleep,就得到了代码的纯效率不同点在于:
1. KeyLock每次针对两个账户进行了lock,而transferAtomic从本质上来说每次只lock一个账户,也就是它将转出和转入当做两个独立的操作进行:这并不影响账面的正确性
2. KeyLock有很多内存分配(装箱操作、Semaphore、包括lock之前的Integer{key1, key2})、Hash查询,而transferAtomic除了初始化之外没有任何内存分配及Hash查询,因而效率更高事实上在需要使用KeyLock的时候总会有更优的替代方法(至少我想不到非得用KeyLock的场景),因为它的本质就是分别锁住两个对象(并没有同时,除非在lock()函数中再次加入同步机制)。
class UserInfo {
String name;
String phone;
}interface UserInfoDao {
// 获取用户信息
UserInfo getUserInfo(int userId); // 更新用户信息
void updateUserInfo(int userId, UserInfo userInfo);
}class UserInfoDaoImpl implements UserInfoDao { @Override
public UserInfo getUserInfo(int userId) {
UserInfo info = null;
// 数据库取数据
return info;
} @Override
public void updateUserInfo(int userId, UserInfo userInfo) {
// 更新数据库
}}//带缓存的DAO
class UserInfoDaoCachedImpl implements UserInfoDao {
ConcurrentMap<Integer, UserInfo> cache;
UserInfoDaoImpl innerDao;
KeyLock lock; @Override
public UserInfo getUserInfo(int userId) {
UserInfo info = cache.get(userId); // 先从缓存取
if (info == null) {
lock.lock(userId);// 缓存无数据则锁定ID
try {
info = innerDao.getUserInfo(userId);// 数据库取
if (info != null) {
cache.put(userId, info); // 更新缓存
}
} finally {
lock.unlock(userId);
}
}
return info;
} @Override
public void updateUserInfo(int userId, UserInfo userInfo) {
lock.lock(userId);
try {
innerDao.updateUserInfo(userId, userInfo);
cache.put(userId, userInfo);
} finally {
lock.unlock(userId);
}
}
}
唯一有问题的时候大概是这样:
T1 updateUserInfo,T2 getUserInfo,此时T1更新了DB却还没更新缓存的时候,T2获取了缓存,与DB不一致。
但是解决这个问题,只要在updateUserInfo的时候先更新缓存不就好了?可能是你的代码没贴全,所以找不到非要这样做的理由
是为了保证缓存和数据库里的数据一致
但问题不是你说的那个。
设想那么个不加锁的场景:
T1和T2同时更新id为0的用户的信息,T1要更新为infoA,T2要更新为infoB
T1先完成innerDao.updateUserInfo(0, infoA);
T2后完成innerDao.updateUserInfo(0, infoB);
这时数据库里存的是infoB
然后
T2先完成cache.put(0, infoB);
T1后完成cache.put(0, infoA);
这时缓存里存的是infoA,缓存里存的和数据库里存的数据不一样如果对整个updateUserInfo加锁,就能保证缓存,数据库一致,单同一时刻只能有一个线程在更新,其他都得等着,哪怕他们更新的不是同一个用户而用KeyLock对ID加锁,既可以保证数据一致又能提高并行度:对相同ID操作的线程互斥,对不同ID操作的线程可以并行。
虽然其加锁开销较大,但比较起数据库操作来说基本可以忽略不计
仔细看了下你的例子,你在没有加锁的情况下先判断账户余额再转账是不安全的
比如账户0有500元,T1和T2都要从账户0中转出300元
T1和T2同时进入余额判断,两线程都满足500 > 300,都进入到了扣钱转账处理,当两线程都处理完成后其账户0的余额是-100元
仔细看了下你的例子,你在没有加锁的情况下先判断账户余额再转账是不安全的
比如账户0有500元,T1和T2都要从账户0中转出300元
T1和T2同时进入余额判断,两线程都满足500 > 300,都进入到了扣钱转账处理,当两线程都处理完成后其账户0的余额是-100元
我的测试代码也忽略了这个,锁应该加在判断之前
锁array + sleep() 效率必须没有KeyLock高
public class KeyLock<T> {
private Map<T, Lock> iLocksMap; private Lock iAllocationLock = new ReentrantLock(); public KeyLock() {
iLocksMap = new ConcurrentHashMap<>();
} public void lock(T key) {
Lock existingLock = iLocksMap.get(key);
if (existingLock != null) {
existingLock.lock();
return;
} try {
iAllocationLock.lock();
Lock newLock = iLocksMap.get(key);
if (newLock == null) {
newLock = new ReentrantLock();
iLocksMap.put(key, newLock);
}
newLock.lock();
return;
}
finally {
iAllocationLock.unlock();
}
} public void unlock(T key) {
Lock existingLock = iLocksMap.get(key);
if (existingLock != null) {
existingLock.unlock();
return;
} try {
iAllocationLock.lock();
existingLock = iLocksMap.get(key);
if (existingLock != null)
existingLock.unlock();
}
finally {
iAllocationLock.unlock();
}
}
}
嗯,仔细看了下你的代码,发现几个小问题,可能你考虑得不是很周全,说实话多线程的东西确实伤脑筋:
1、iLocksMap没有进行清理,因为key的范围是不限制的,所以map也会无限制增长
2、并不是公平锁,公平锁是先请求的先得到锁,new ReentrantLock(true)才是公平锁,new ReentrantLock()是非公平的。你用这个当内部锁控制,那必然也是非公平的
3、某些情况下不同key也互斥,也就是一个线程锁定key1另一线程无法锁定key2。这个情况有点复杂,结合你的KeyLock代码行数来说明: T1请求锁定keyA,执行到22行,将newLock放入了iLocksMap,但还未执行23行
这时T2请求锁定相同keyA,执行到13行,获得existingLock锁,然后return
T1等待在了24行
T3请求锁定keyB,等待在了18行,因为iAllocationLock被T1获取,T1还等在24行
这个情况下T3请求的keyB与T1、T2请求的keyA是不可并行的,T3必须等待T1释放、T2获得、T2释放后才能得到18行的iAllocationLock
T1请求锁定keyA,执行到22行,将newLock放入了iLocksMap,但还未执行23行
这时T2请求锁定相同keyA,执行到13行,获得existingLock锁,然后return
T1等待在了24行
T2再次请求锁定keyB,等待在了18行,因为iAllocationLock被T1获取,T1还等在24行
这个情况下
T1获得iAllocationLock在等待newLock
T2获得existingLock在等待iAllocationLock
由于newLock和existingLock是同一个锁,所以死锁了
这段代码明显有问题:
// ...
if (iAccounts[from].get() < money)
return;
iAccounts[from].getAndAdd(-money);
// ...
import java.util.concurrent.ConcurrentHashMap;public class KeyLocks<L> {
private final Map<L, Object> locks = new ConcurrentHashMap<L, Object>();
public Object getLock(L key) {
Object result = locks.get(key);
if( result == null ) synchronized (locks) {
result = locks.get(key);
if( result == null ) {
result = new Object();
locks.put(key, result);
}
}
return result;
}
}class Test {
private final int[] accounts;
private final KeyLocks<Integer> keyLocks;
Test(int count, int money) {
if( count <= 0 )
throw new IllegalArgumentException("Invalid number of accounts: " + count);
if( money < 0 )
throw new IllegalArgumentException("Invalid initial balance: " + money);
accounts = new int[count];
for(int i=0; i<accounts.length; i++)
accounts[i] = money;
keyLocks = new KeyLocks<Integer>();
}
boolean transfer(int from, int to, int money) {
if( from < 0 || from >= accounts.length )
throw new IndexOutOfBoundsException("Invalid from account: " + from);
if( to < 0 || to >= accounts.length )
throw new IndexOutOfBoundsException("Invalid to account: " + to);
if( from == to )
throw new IllegalArgumentException("Cannot transfer between same account.");
if( money < 0 )
throw new IllegalArgumentException("Invalid transfer amount: " + money);
synchronized (keyLocks.getLock(from)) {
if( accounts[from] < money )
return false;
accounts[from] -= money;
}
synchronized (keyLocks.getLock(to)) {
accounts[to] += money;
}
return true;
}
}
我不知道每次访问 ThreadLocal 和 建立 Semaphore 实例的开销有多大,也许是可以忽略不计的,那么这两种实现也许效率差不多。
如果把try里面的newlock.lock放到finally之后貌似就行了
同意你说的,如果具体到某场景应该总有更优化的实现方式。AtomicInteger 的问题在于,它不能支持 “读取,检查然后决定是否写入” 这种复杂组合的原子性保护。
测过开销,KeyLock加锁开销约为ReentrantLock的3倍,所以其比较适用于处理耗时长的情景你的实现也没有考虑到locks map的释放,因为key的范围是不确定的,在运行期间可能处理成千上万个不同的key,可以考虑考虑java的虚引用,也许有方法解决你代码里locks的释放
public class KeyLock<T> {
private final Map<T, Lock> iLocksMap; private final boolean iFair; private Lock iAllocationLock; public KeyLock(boolean fair) {
iFair = fair;
iLocksMap = new ConcurrentHashMap<>();
iAllocationLock = new ReentrantLock(iFair);
} public void lock(T key) {
Lock existingLock = iLocksMap.get(key);
if (existingLock != null) {
existingLock.lock();
return;
} Lock newLock = null;
try {
iAllocationLock.lock();
newLock = iLocksMap.get(key);
if (newLock == null) {
newLock = new ReentrantLock(iFair);
iLocksMap.put(key, newLock);
}
return;
}
finally {
iAllocationLock.unlock();
newLock.lock();
}
} public void unlock(T key) {
Lock existingLock = iLocksMap.get(key);
if (existingLock != null) {
existingLock.unlock();
return;
} try {
iAllocationLock.lock();
existingLock = iLocksMap.get(key);
if (existingLock != null)
existingLock.unlock();
}
finally {
iAllocationLock.unlock();
}
} public void dumpLock(T key) {
if (iLocksMap.remove(key) != null)
return; try {
iAllocationLock.lock();
if (iLocksMap.remove(key) != null)
return;
}
finally {
iAllocationLock.unlock();
} iLocksMap.remove(key);
}
private int[] accounts;
private KeyLock<Integer> lock = new KeyLock<Integer>();
public boolean transfer(int from, int to, int money) {
Integer[] keys = new Integer[] {from, to};
Arrays.sort(keys); //对多个key进行排序,保证锁定顺序防止死锁
lock.lock(keys);
try {
//处理不同的from和to的线程都可进入此同步块
if (accounts[from] < money)
return false;
accounts[from] -= money;
accounts[to] += money;
return true;
} finally {
lock.unlock(keys);
}
}
(顺便,里面的 KeyLock<Integer> 应该是 KeyLock<Integer[]> 吧)假设帐号 3 有 500
如果有两个操作同时发生在两个线程里:从 3 到 5 转帐 300
从 3 到 6 转帐 300由于 {3,5} 和 {3,6} 是不相等的两把锁,会导致两个线程可能同时访问到 帐号3, 就像3没有被加锁保护一样。
Lock existingLock = iLocksMap.get(key);
if (existingLock != null) {
existingLock.unlock();
return;
} try {
iAllocationLock.lock();
existingLock = iLocksMap.get(key);
}
finally {
iAllocationLock.unlock();
if (existingLock != null)
existingLock.unlock();
}
}
这段代码中调用的是这个方法 /**
* 锁定多个key
* 建议在调用此方法前先对keys进行排序,使用相同的锁定顺序,防止死锁发生
* @param keys
*/
public void lock(K[] keys) {
if (keys == null)
return;
for (K key : keys) {
lock(key);
}
} 分别请求两个key的锁,而不是将Integer[]作为请求的锁
测过开销,KeyLock加锁开销约为ReentrantLock的3倍,所以其比较适用于处理耗时长的情景你的实现也没有考虑到locks map的释放,因为key的范围是不确定的,在运行期间可能处理成千上万个不同的key,可以考虑考虑java的虚引用,也许有方法解决你代码里locks的释放对GC机制不熟悉,如果一个线程在使用某 Object 的 monitor, 算不算强引用? 如果算的话,也许可以用类似 WeakHashMap 的手段。
这段代码中调用的是这个方法 /**
* 锁定多个key
* 建议在调用此方法前先对keys进行排序,使用相同的锁定顺序,防止死锁发生
* @param keys
*/
public void lock(K[] keys) {
if (keys == null)
return;
for (K key : keys) {
lock(key);
}
} 分别请求两个key的锁,而不是将Integer[]作为请求的锁楼上
测过开销,KeyLock加锁开销约为ReentrantLock的3倍,所以其比较适用于处理耗时长的情景你的实现也没有考虑到locks map的释放,因为key的范围是不确定的,在运行期间可能处理成千上万个不同的key,可以考虑考虑java的虚引用,也许有方法解决你代码里locks的释放对GC机制不熟悉,如果一个线程在使用某 Object 的 monitor, 算不算强引用? 如果算的话,也许可以用类似 WeakHashMap 的手段。这个我也不是很清楚,如果能用其实现异步清理的话,再加上是使用对象内部锁,那么其性能肯定会比我的实现方案好很多
测过开销,KeyLock加锁开销约为ReentrantLock的3倍,所以其比较适用于处理耗时长的情景你的实现也没有考虑到locks map的释放,因为key的范围是不确定的,在运行期间可能处理成千上万个不同的key,可以考虑考虑java的虚引用,也许有方法解决你代码里locks的释放对GC机制不熟悉,如果一个线程在使用某 Object 的 monitor, 算不算强引用? 如果算的话,也许可以用类似 WeakHashMap 的手段。这个我也不是很清楚,如果能用其实现异步清理的话,再加上是使用对象内部锁,那么其性能肯定会比我的实现方案好很多我看了一下ReentrantLock的源码,没有发现它将自己的引用传递给底层。底层引用的是NonfairSync或FairSync,虽然它们都是ReentrantLock的内部类,但它们是static的,所以也没有依赖于ReentrantLock的实例。我估计就算线程hold住lock,那这个lock本身还是可以被垃圾回收的(假设没有任何强引用)。可以做个试验看看。
效率不高倒是肯定的,排序倒是只用传个Comparator就行,用不着KEY有自然顺序,只要保证所有处理用相同顺序请求就行。好在我真实应用中不需要同时锁多个KEY
大概有点超出了我的能力范围,想了半天没有想出一个试验的方案……
刚刚google了一下没有找到现成的答案,翻了一下 JLS 7 和 oracle的 JVM specification 也没找到答案……
测过开销,KeyLock加锁开销约为ReentrantLock的3倍,所以其比较适用于处理耗时长的情景你的实现也没有考虑到locks map的释放,因为key的范围是不确定的,在运行期间可能处理成千上万个不同的key,可以考虑考虑java的虚引用,也许有方法解决你代码里locks的释放对GC机制不熟悉,如果一个线程在使用某 Object 的 monitor, 算不算强引用? 如果算的话,也许可以用类似 WeakHashMap 的手段。这个我也不是很清楚,如果能用其实现异步清理的话,再加上是使用对象内部锁,那么其性能肯定会比我的实现方案好很多我看了一下ReentrantLock的源码,没有发现它将自己的引用传递给底层。底层引用的是NonfairSync或FairSync,虽然它们都是ReentrantLock的内部类,但它们是static的,所以也没有依赖于ReentrantLock的实例。我估计就算线程hold住lock,那这个lock本身还是可以被垃圾回收的(假设没有任何强引用)。可以做个试验看看。
大概有点超出了我的能力范围,想了半天没有想出一个试验的方案……
刚刚google了一下没有找到现成的答案,翻了一下 JLS 7 和 oracle的 JVM specification 也没找到答案……我先说一下实验方案,然后我们一起做。
建一个MyReetrantLock类继承ReentrantLock,并重写finalize方法让它输出一个提示
用WeakReference建立一个锁,然后让一个线程锁住它。
这个线程不停地创建新对象并放入一个ArrayList,迫使JVM回收垃圾
间或调用一下System.gc()
最后释放锁,清空ArrayList,再重复一下之前的步骤,再睡一会儿。
最后看输出
public class Father { public static void main(String args[]) {
new Thread(new Runnable() {
@Override
public void run() {
MyLock strongLock = new MyLock();
WeakReference<MyLock> weakLock = new WeakReference<MyLock>(strongLock);
strongLock.lock();
System.out.println("locked");
strongLock = null; System.out.println("Allocation like crazy");
List<String> garbage = new ArrayList<String>();
for (int i = 0; i < 1000000; i++) {
garbage.add(String.valueOf(i));
} System.out.println("calling gc");
System.gc(); try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
//ignore
} MyLock lock = weakLock.get();
if (lock == null) {
System.out.println("damn!");
return;
}
else {
lock.unlock();
} garbage.clear(); System.out.println("Allocation like crazy again");
for (int i = 1000000; i < 2000000; i++) {
garbage.add(String.valueOf(i));
} System.out.println("calling gc");
System.gc();
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
// ignore
} if (weakLock.get() == null)
System.out.println("hooray!!");
}
}).start();
} private static class MyLock extends ReentrantLock {
@Override
public void finalize() {
System.out.println("Hi this is me, a lock! But I'm dying, see ya");
}
}
}测试结果:
locked
Allocation like crazy
Hi this is me, a lock! But I'm dying, see ya
calling gc
damn!证明Lock即便被线程拥有也会被回收(看来我之前估计得没错。。)
lock.lock(hash);
try {
//do something
} finally {
lock.unlock(hash);
}
这本书不错。
lock.lock(hash);
try {
//do something
} finally {
lock.unlock(hash);
}没看懂。。解决了啥问题?
lock.lock(hash);
try {
//do something
} finally {
lock.unlock(hash);
}没看懂。。解决了啥问题?
key只用实现hashCode方法就行,意思是用int值对key进行映射,然后锁其int值
不会这样用hash加锁唯一会出现的情况是,两个不同的key它们的hash值一样,两个线程对这两个key同时操作时会互斥,因为KeyLock认的是其hash值
用hash来锁定还有一个好处,其KeyLock内部的MAP效率更高,因为判断相等是用的Integer,其hashCode和equals方法效率高
这样的话,就不能随意释放map中的锁了……
不会这样用hash加锁唯一会出现的情况是,两个不同的key它们的hash值一样,两个线程对这两个key同时操作时会互斥,因为KeyLock认的是其hash值
用hash来锁定还有一个好处,其KeyLock内部的MAP效率更高,因为判断相等是用的Integer,其hashCode和equals方法效率高o,你的意思是让外部代码来调用hashCode,我以为是在KeyLock内部进行这样的处理。。
不管是内部还是外部这样做肯定不行的,原因就是你说的那条hash值冲突问题。在这个前提下讨论效率没什么意义了,因为正确性是第一位的。当然如果是你说的那种特殊应用场景,那是没问题,而且甚至应该用trove的TIntegerObjectMap来替代你的ConcurrentMap,效率更高。
public class KeyLock2<T> {
private final Map<T, Lock> iLocksMap; private final boolean iFair; private Lock iAllocationLock; private Lock iFreeMapLock; private ScheduledExecutorService iExecutor; public KeyLock2(boolean fair, long collectionRateMilli) {
iFair = fair;
iLocksMap = new ConcurrentHashMap<T, Lock>();
iAllocationLock = new ReentrantLock(iFair); iFreeMapLock = new ReentrantLock();
iExecutor = Executors.newScheduledThreadPool(1); iExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
dumpLocks();
}
}, 0, collectionRateMilli, TimeUnit.MILLISECONDS);
} public void lock(T key) {
Lock newLock = null;
try {
iAllocationLock.lock();
newLock = iLocksMap.get(key);
if (newLock == null) {
newLock = new ReentrantLock(iFair);
iLocksMap.put(key, newLock);
}
return;
}
finally {
iAllocationLock.unlock();
if (newLock != null)
newLock.lock();
}
} public void unlock(T key) {
Lock existingLock;
try {
iAllocationLock.lock();
existingLock = iLocksMap.get(key);
}
finally {
iAllocationLock.unlock();
} if (existingLock != null)
existingLock.unlock();
} public void dumpLocks() {
iFreeMapLock.lock();
try {
for (Iterator<Map.Entry<T, Lock>> entryIter = iLocksMap.entrySet().iterator(); entryIter.hasNext();/*empty*/) {
Map.Entry<T, Lock> entry = entryIter.next();
if (entry.getValue().tryLock()) {
iAllocationLock.lock();
try {
entryIter.remove();
}
finally {
iAllocationLock.unlock();
}
}
}
}
finally {
iFreeMapLock.unlock();
}
}
}
每个KeyLock对象就包含一清理线程,程序里可不止只有一个KeyLock对象,有对人员ID加锁的,有对工单ID加锁的,有对
private int iKey;
private int iType;
private LockKey (int key, int type) {
iKey = key;
iType = type;
} public static getIDLockKey (int key) {
return new LockKey(key, 0);
} public static getBillLockKey (int key) {
return new LockKey(key, 1);
} public int getKey() {
} public boolean equals (Object o) {
if (!o instanceof LockKey)
return false;
return (LockKey) o.iKey == iKey
}
}
private int iKey;
private int iType;
private LockKey (int key, int type) {
iKey = key;
iType = type;
} public static getIDLockKey (int key) {
return new LockKey(key, 0);
} public static getBillLockKey (int key) {
return new LockKey(key, 1);
} public int getKey() {
return iKey;
} public boolean equals (Object o) {
if (o == null || !o instanceof LockKey)
return false;
return ((LockKey) o).iKey == iKey && ((LockKey) o).iType == iType;
} public int hashCode() {
return iKey * 17 + (iKey << iType) * 17;
}
}如此就可以公用KeyLock啦