java高并发下锁的用法

半兽人 发表于: 2018-01-05   最后更新时间: 2021-09-06 20:16:22  
{{totalSubscript}} 订阅, 6,508 游览

提到锁,大家可能都会想到synchronized关键字,使用它的确可以解决一切并发问题,但是对于系统吞吐要求更高的,在这里提供了几个小技巧,帮助大家减小锁粒度,提高系统并发能力。

初级 - 乐观锁

乐观锁适合这样的场景:读不会冲突,写会冲突。同时读的频率远大于写。

以下面的代码为例,悲观锁的实现:

public Object get(Object key) {  
   synchronized(map) {  
      if(map.get(key) == null) {  
         // set some values  
      }  

       return map.get(key);  
   }  
}

乐观锁的实现:

public Object get(Object key) {  
   Object val = null;  
   if((val = map.get(key) == null) {  
       // 当map取值为null时再加锁判断  
       synchronized(map) {  
           if(val = map.get(key) == null) {  
               // set some value to map...  
           }  
        }  
   }  

    return map.get(key);  
}

中级 - String.intern()

乐观锁不能很好解决大量写冲突问题,但是如果很多场景下,锁实际上只是针对某个用户或者某个订单。比如一个用户必须先创建session,才能进行后面的操作。但是由于网络原因,创建用户session的请求和后续请求几乎同时达到,而并行线程可能会先处理后续请求。一般情况,需要对用户sessionMap加锁,比如上面的乐观锁。在这种场景下,可以讲锁限定到用户本身上,即从原来的

lock.lock();
    int num=storage.get(key);
    storage.set(key,num+1);
lock.unlock();
更改为:
lock.lock(key);
    int num=storage.get(key);
    storage.set(key,num+1);
lock.unlock(key);

这个比较类似于数据库表锁和行锁的概念,显然行锁的并发能力比表锁高很多。

使用String.inter()是这种思路的一种具体实现。类 String 维护一个字符串池。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。可见,当String相同时,String.intern()总是返回同一个对象,因此就实现了对同一用户加锁。由于锁的粒度局限于具体用户,使系统获得了最大程度的并发。

public void doSomeThing(String uid) {  
   synchronized(uid.intern()) {  
       // ...  
   }  
}

CopyOnWriteMap?

既然说到了“类似于数据库中的行锁的概念”,就不得不提一下MVCC,Java中CopyOnWrite类实现了MVCC。Copy On Write是这样一种机制。当我们读取共享数据的时候,直接读取,不需要同步。当我们修改数据的时候,我们就把当前数据Copy一份副本,然后在这个副本上进行修改,完成之后,再用修改后的副本,替换掉原来的数据。这种方法就叫做Copy On Write。

但是,JDK并没有提供CopyOnWriteMap,为什么?下面有个很好的回答,那就是已经有了ConcurrentHashMap,为什么还需CopyOnWriteMap?

Fredrik Bromee 写道
I guess this depends on your use case, but why would you need a CopyOnWriteMap when you already have a > ConcurrentHashMap?

For a plain lookup table with many readers and only one or few updates it is a good fit.

Compared to a copy on write collection:

Read concurrency:

Equal to a copy on write collection. Several readers can retrieve elements from the map concurrently in a lock> -free fashion.

Write concurrency:

Better concurrency than the copy on write collections that basically serialize updates (one update at a > time). Using a concurrent hash map you have a good chance of doing several updates concurrently. If your hash > keys are evenly distributed.

If you do want to have the effect of a copy on write map, you can always initialize a ConcurrentHashMap with a concurrency level of 1.

转译过来就是:

Fredrik Bromee 写道:
我想这取决于你的使用情况,但是当你已经有一个ConcurrentHashMap的时候,你就不需要CopyOnWriteMap了。

对于读取多的,只有一个或几个更新的,这是一个很好的选择。

那么与CopyOnWrite相比:

并发读:

等于写时复制集合。读可以以无锁的方式同时从map中检索元素。

写并发:

与基与序列化更新的写时复制集合相比(一次更新一个),具有更好的并发性。如果你的散列键均匀分布,使用并发hash map可以同时进行多个更新。

如果你希望具有copy on write map的效果,则可始终设置初始化并发级别为1的ConcurrentHashMap即可。

另外,简单说下什么是CopyOnWrite

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复>制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

高级 - 类ConcurrentHashMap

String.inter()的缺陷是类 String 维护一个字符串池是放在JVM perm区的,如果用户数特别多,导致放入字符串池的String不可控,有可能导致OOM错误或者过多的Full GC。怎么样能控制锁的个数,同时减小粒度锁呢?直接使用Java ConcurrentHashMap?或者你想加入自己更精细的控制?那么可以借鉴ConcurrentHashMap的方式,将需要加锁的对象分为多个bucket,每个bucket加一个锁,伪代码如下:

Map locks = new Map();  
List lockKeys = new List();  
for(int number : 1 - 10000) {  
   Object lockKey = new Object();  
   lockKeys.add(lockKey);  
    locks.put(lockKey, new Object());  
}  

public void doSomeThing(String uid) {  
   Object lockKey = lockKeys.get(uid.hash() % lockKeys.size());  
   Object lock = locks.get(lockKey);  

   synchronized(lock) {  
      // do something  
   }  
}
更新于 2021-09-06

查看java更多相关的文章或提一个关于java的问题,也可以与我们一起分享文章