java遇见高并发使用锁实现并发安全

本文介绍了Java中两种常见的锁机制——Lock锁和synchronized锁,详细阐述了它们的工作原理及应用场景。Lock锁分为公平锁和非公平锁,适合写入频繁的场景;synchronized锁作为悲观锁,适用于读取频繁的情况。通过String.intern()方法举例说明了如何细化锁的粒度,并提到了ConcurrentHashMap的锁分段策略。最后总结指出,选择锁应根据并发场景的读写比例和冲突概率来决定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一种Lock 锁

//公平锁
Lock lock1=new ReentrantLock(true);
举个例子,当A线程获得了锁开始访问同步资源的时候,B线程来了,因为锁没有释放,所以B开始等待,等待一段时间后,C线程也来了,俩线程都在等待,这个时候A释放锁了,根据先来后到的道理,B优先获得了锁。
//非公平锁
Lock lock2=new ReentrantLock(false);
那么非公平锁就不言而喻了。
这个时候线程B和线程C获得锁的几率是一样的。

//Lock是手动锁,加锁和释放必须得手动进行
public class TestLock {
  //创建锁对象
    Lock lock=new ReentrantLock(true);
   public void m1(){
        try {
        	//加上锁
            lock.lock();
            for(int i=0;i<10;i++){
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            {
            //释放
                lock.unlock();
            }
        }
    }
}

第二种synchronized 锁

初级技巧 - 悲观锁–乐观锁

1.悲观锁
比较适合写入操作比较频繁的场景, 如果出现大量的读取操作, 每次读取的时候都会进行加锁, 这样会增加大量的锁的开销, 降低了系统的吞吐量

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

2.乐观锁
乐观锁适合这样的场景:读不会冲突,写会冲突。同时读的频率远大于写。
比较适合读取操作比较频繁的场景, 如果出现大量的写入操作, 数据发生冲突的可能性就会增大, 为了保证数据的一致性, 应用层需要不断的重新获取数据, 这样会增加大量的查询操作, 降低了系统的吞吐量

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()) {
       // ...
   }
}

高级技巧 - 类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
   }
}

小结: 两者各有优缺点, 读取频繁使用乐观锁, 写入频繁使用悲观锁

​ 像乐观锁适用于写入比较少的情况下, 即冲突真的很少发生的时候, 这样省去了锁的开销, 加大了系统的吞吐量, 但如果经常产生冲突, 上层应用会不断的进行retry, 这样反倒是降低了性能, 所以这种情况下用悲观锁就比较合适, 之所以用悲观锁就是因为两个用户更新同一条数据的概率高, 也就是冲突比较严重的情况下所以才用悲观锁

​悲观锁比较适合强一致性的场景, 但效率比较低, 特别是读的并发低, 乐观锁则适用于读多写少, 并发冲突少的场景!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值