Java线程锁&分布式锁的理解及应用

时间:2023-03-09 09:26:20
Java线程锁&分布式锁的理解及应用

  了解Java线程锁之前,先理解线程和进程的定义。进程是操作系统分配资源(CPU)的基本单位,线程是CPU执行的基本单位,一个进程可拥有多个线程,同进程间的多个线程共享分配给进程的资源。比如启动JVM时,会拥有一个进程,JVM处理并发请求的线程共享JVM的堆内存资源。

  进程间的通信:网络通信,比如RPC,MQ,Socket。

  线程间的通信:由于多线程共享地址空间和数据空间,因此同进程间的多个线程的通信是任意线程数据可以直接提供其他线程使用,而不必通过操作系统。

  Java线程的状态:初始(New),运行(Runnable),阻塞(Blocked),等待(Waiting),超时等待(Time_Waiting),终止(Teminate)。

  1. 初始:新创建的一个线程对象,但还没有调用start()方法。

  2. 运行:Java线程中将就绪(ready)和运行中(Running)俩种状态笼统称为“运行”。

  3. 阻塞:表示线程阻塞于锁。

  4. 等待:进入该状态的线程需要其他线程作出一些特定动作(通知或中断)来激活或结束。

  5. 超时等待:该状态等同于Waiting,它可以在制定的时间内自行返回,比如sleep()方法。

  6. 终止:表示该线程已经执行完毕

Java线程锁&分布式锁的理解及应用

  通过线程的6大状态,能看出线程锁主要出现在阻塞(Blocked),什么情况下会用到线程锁?比如并发情况下为了控制资源争抢问题或者实现互斥效果,会进行locked,也就是把并发控制为同步。

  既然线程锁已能实现同步控制,为什么还要衍生出分布式锁?因为所谓的线程锁解决的是同一个进程中的并发锁问题,不同进程中存在资源可见性的问题。进程间的资源是不共享的,也就是不可见的。比如在不同端口启动多个JVM(多个进程),它们之间的共享资源(内存)和线程锁都是互不可见的。从而分布式系统中,也是在分布在不同机器中,同进程的线程锁是无法控制并发问题的。

  分布式锁应该具备的条件:

  1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
  2. 高可用的获取锁与释放锁; 
  3. 高性能的获取锁与释放锁; 
  4. 具备可重入特性; 
  5. 具备锁失效机制,防止死锁; 
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

  如何在分布式系统中解决最基本的资源争抢或者数据覆盖的问题,理论上来说解决此问题,最基本需解决锁可见性的问题,也就是共享资源。比如基于数据库的锁机制,基于缓存(redis)的分布式锁,以及基于zookeeper实现的分布式锁。

  1. 基于数据库的锁机制实现的分布式锁

  作为数据库本身已有一套健全的锁机制(更新),同时是基于外部的磁盘存储的逻辑(另外一个进程),完全符合分布式锁的需求,但由于锁资源存储在磁盘中,需进行I/O操作,因此相对来说性能不高。

  比如:select * from tb_lock where id = 3 for update,当执行这个语句时,数据库会自动对id=3这一行数据加行级锁(id为主键,否则为表级锁),其它事务必须等待此次事务提交或回滚后才能执行。

  2. 基于缓存(redis)实现的分布式锁

  由于redis的单进程单线程的原理,同时也时外部缓存的存储逻辑,也是能作为分布式锁的底层基础。同时expire的过期功能,又完美的解决了特殊原因导致锁无法释放的问题。

  github上的redisson开源项目已实现了分布式锁功能,同时也解决了redis的重入锁问题。也可通过LUA脚本+redis实现单线程事务控制。

  3. 基于zookeeper实现的分布式锁

  基于zookeeper临时有序节点的大致思想:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应指定节点的目录下,生成一个唯一的瞬时节点。判断是否获取锁的逻辑是只需要判断有序节点序号最小的一个。当释放锁的时,只需将这个瞬时节点删除即可。同时,其可避免服务宕机导致的锁无法释放,而产生的死锁问题。