参考文献

锁的分类

  • 分类标准把锁分为以下 7 大类别
    • 偏向锁/轻量级锁/重量级锁;
    • 可重入锁/非可重入锁;
    • 共享锁/独占锁;
    • 公平锁/非公平锁;
    • 悲观锁/乐观锁;
    • 自旋锁/非自旋锁;
    • 可中断锁/不可中断锁.

使用锁的目的

  • 锁是用来控制多个线程访问共享资源的方式.一个锁能够防止多个线程同时访问共享资源.

死锁

产生死锁的原因

  • 当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,同时不放弃自己拥有的资源;

如何预防死锁

  • 只有以下这四个条件都发生时才会出现死锁:
    • 互斥:共享资源X和Y只能被一个线程占用;
    • 占有且等待: 线程T1已取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
    • 不可抢占: 其他线程不能强行抢占线程T1占有的资源;
    • 循环等待: 线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待.
  • 即破坏其中一个,就可以成功避免死锁的发生
    • 对于"占用且等待"这个条件,可以一次性申请所有资源,这样就不存在等待了.
    • 对于"不可抢占"这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破了.
    • 对于"循环等待"这个条件,可以按序申请资源来预防.所谓按序申请是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的.
  • 当多个线程持有不同的锁并试图获取对方持有的锁时,就会发生死锁问题.以下是一些避免死锁问题的方法:
    1. 避免多个锁的嵌套使用.如果必须使用多个锁,则应该确定所有线程获取锁的顺序,并确保所有线程以相同的顺序获取锁.
    2. 使用可重入锁.可重入锁可以允许同一个线程多次获取同一个锁,因此不会因为自身持有锁而导致死锁.
    3. 使用定时锁.在使用锁的时候,可以设置一个超时时间,如果在规定时间内无法获取锁,则放弃锁的获取,避免因等待过长时间而引起死锁.
    4. 使用并发集合类.Java中的ConcurrentHashMapConcurrentSkipListMap等并发集合类可以避免锁的嵌套使用,从而减少死锁的可能性.
    5. 尽量避免在持有锁的情况下调用外部方法.这可能会导致锁的嵌套,从而增加死锁的风险.
    6. 避免在线程持有锁的情况下阻塞.如果一个线程持有锁并在等待某个资源时阻塞了,那么其他线程也就不能进入临界区,这可能会导致死锁.

定位死锁

  • 检测死锁可以使用jconsole工具或者jps定位进程id,再用jstack定位死锁.
    • 使用jps查看运行的Java进程: jps -l
    • 使用jstack查看线程堆栈信息: jstack -l 进程id
  • jstack 命令: jstack 是 Java 虚拟机提供的命令行工具,它可以用于打印出虚拟机中所有线程的堆栈信息,如果某些线程处于 BLOCKED(阻塞)状态,并且相互之间形成了闭环,就表示发生了死锁.通过jstack 命令输出的信息可以帮助我们定位死锁问题.
  • jconsole 工具: jconsole 是 Java 虚拟机提供的一款监控工具,它可以用于检查 Java 程序的运行状态,包括线程状态.在 jconsole 中,我们可以查看所有线程的状态,如果发现有多个线程处于 BLOCKED 状态,并且相互等待对方释放锁,就很可能发生了死锁.

活锁

  • 出现在两个线程互相改变对象的结束条件,最终导致谁也无法结束.
  • 活锁往往出现在多个线程之间协调处理共同任务时,由于过度的互动而发生.比较常见的例子是在“礼让”同步算法中出现活锁的问题.在这种算法中,一个线程让另一个线程先执行,但当两个线程都使用相同策略(如同时放弃执行并重新尝试)时,就会导致活锁的出现.
  • 为避免活锁的出现,可以使用以下几种方法:
    1. 引入随机因素:在线程的等待时间或其他操作中加入一些随机因素,以避免多个线程以相同的方式相互作用.
    2. 使用时间戳:每个线程使用不同的时间戳来控制自己的行为,可以避免多个线程同时执行相同的操作.
    3. 调整算法:尝试调整算法,减少线程的相互依赖性,避免线程之间的反复交互.
    4. 降低负载:在高负载的情况下,线程相互作用的频率较高,更容易导致活锁的出现.因此,可以通过减少负载来降低线程之间的互动次数,从而减少活锁发生的可能性.

饥饿

  • 一个线程由于优先级太低,始终得不到CPU调用执行,也不能够结束.

  • 解决办法: 顺序加锁