高并发场景下,如果需要锁的存在,可以视对锁的需求来处理,比如仅做数值统计,条件翻转,严格串行,逻辑唯一,允许重试等。不同的情况有不同的处理方式。
数值统计&条件翻转
针对数值统计,可以依赖Atomic下的各种计数器来实现。但是,如果竞争线程非常多,竞争激烈的场合,还可以进一步使用比如LongAdder。其通过锁分裂的机制,支持更高的并发能力。
严格串行
这种场景下,一般都需要Synchronized或者ReentrantLock来支持。可以更进一步的进行细分。
比如:是否竞争激烈,是否有超时走分支逻辑的需求,是否需要分条件挂起线程等。
按竞争激烈程度,如果竞争不够激烈,可以使用Synchronized,其存在锁膨胀的机制,在竞争不激烈,锁持有时间很短的情况下,可以保持线程的活性,尽快获取锁。当然,ReentrantLock也可以通过乐观锁机制配合自旋来接近Synchronized实现。具体看哪种使用更顺手一点。
如果竞争激烈,存在超时分支逻辑,那需要用ReentrantLock,通过tryLock的乐观锁超时机制来支持。
如果阻塞的情况分多种,需要将线程按条件挂起,那需要用到Condition。通过多个Condition来区分不同的条件场景,然后在合适的场合挂起线程。
逻辑唯一
这个一般指幂等场合。比如:用户生成多笔订单,每一个订单会在用户的积分账户增加若干积分。在具体累计积分时,在数据库层面会做乐观锁保护,但是竞争乐观锁失败的请求完全可以自旋重试即可。因为消息的处理节点可能是集群并行,也可能单机并发处理。故这种场景下,一般不会加锁。而是通过数据的乐观锁结合唯一键来支持。在这个基础上,再视情况引入自旋来支持。
除了上述场合外,在ConcurrentHashMap中也存在类似的语义函数。putIfAbsent,可以支持并发执行下,仅有一个线程插入成功,返回null,其它线程返回旧值。
允许重试
这个和CAS很像,一次判断失败,可以通过若干次自旋重试来支持。不过和CAS相比,更多是针对代码块语义。具体是否允许,取决于自旋的收益。