多线程与死锁

超、凢脫俗 2022-02-03 11:23 485阅读 0赞

多线程与死锁

  • 动态顺序锁死锁
  • Jconsole 查看死锁
  • 固定锁顺序避免死锁
  • 协作锁之间发生死锁
  • 开放调用避免死锁
  • RetreenLock锁超时解决死锁

Java多线程开发中,为了避免多个线程对同一份数据的操作,我们需要对我们的线程做加锁的操作,只要加锁,就必然存在锁竞争的问题,如果锁竞争的问题处理不当就会出现死锁问题。死锁会让程序一直卡住,程序不再往下执行。我们只能通过中止并重启的方式来让程序重新执行。

这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!

造成死锁的原因可以概括成三句话:

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

动态顺序锁死锁

动态顺序锁导致死锁是最常见的死锁,例如我们在2个线程对2个账户进行转账操作,我们需要先锁定汇账账户减钱,然后再锁定入款账户加钱,一般来讲这个逻辑顺序是没有问题的,但是如果这2个线程并发同时处理,就会产生死锁。

  1. public class DeadLock {
  2. public static void main(String[] args) {
  3. Account accountA = new Account(1L,"A",10000L);
  4. Account accountB = new Account(1L,"B",10000L);
  5. Thread a = new Thread(()->{
  6. try {
  7. transferMoney(accountA,accountB,100L);
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. });
  12. Thread b = new Thread(()->{
  13. try {
  14. transferMoney(accountB,accountA,100L);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. });
  19. a.start();
  20. b.start();
  21. while (true){
  22. try {
  23. Thread.sleep(1000);
  24. }catch (Exception e){
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. // 转账
  30. public static void transferMoney(Account fromAccount,
  31. Account toAccount,
  32. Long amount) throws Exception {
  33. // 锁定汇账账户
  34. synchronized (fromAccount) {
  35. System.out.println(Thread.currentThread().getName()+"获取账户"+fromAccount.getName()+"锁");
  36. // 锁定来账账户
  37. synchronized (toAccount) {
  38. System.out.println(Thread.currentThread().getName()+"获取账户"+toAccount.getName()+"锁");
  39. // 判余额是否大于0
  40. if (fromAccount.getAmount().compareTo(amount) < 0) {
  41. throw new Exception("No enough money");
  42. } else {
  43. // 汇账账户减钱
  44. fromAccount.debit(amount);
  45. // 来账账户增钱
  46. toAccount.credit(amount);
  47. System.out.println(Thread.currentThread().getName()+"完成转账");
  48. }
  49. }
  50. }
  51. }
  52. }
  53. @Data
  54. class Account{
  55. private Long id;
  56. private String name;
  57. private Long amount;
  58. public Account(){ }
  59. public Account(long id,String name, long amount){
  60. this.id = id;
  61. this.name = name;
  62. this.amount = amount;
  63. }
  64. public void debit(Long amount) {
  65. this.amount -= amount;
  66. }
  67. public void credit(Long amount) {
  68. this.amount += amount;
  69. }
  70. }

程序执行结果如下:

  1. Thread-1获取账户B
  2. Thread-0获取账户A

我们会发现我们的转账一直无法完成交易,程序一直卡住,程序不再往下执行。即发生了死锁。

Jconsole 查看死锁

Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,我们可以直接查看Java进程中出现的死锁。
控制台输入jconsole启动图形化界面工具

  1. jconsole

在这里插入图片描述
选择连接我们的本地调试进程
在这里插入图片描述
选择线程栏,我们会看到我们的正在运存的测试代码的3个线程
在这里插入图片描述
选择我们的线程,检测死锁
在这里插入图片描述
在这里插入图片描述
很明显看出,Thread-0需要的资源被Thread-1占用,Thread-0被阻塞。

固定锁顺序避免死锁

针对动态锁顺序导致的死锁,我们可以通过固定加锁的顺序来解决

  1. public class FixedOrderDeadLock {
  2. public static void main(String[] args) {
  3. Account accountA = new Account(1L,"A",10000L);
  4. Account accountB = new Account(1L,"B",10000L);
  5. Thread a = new Thread(()->{
  6. try {
  7. transferMoney(accountA,accountB,100L);
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. });
  12. Thread b = new Thread(()->{
  13. try {
  14. transferMoney(accountB,accountA,100L);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. });
  19. a.start();
  20. b.start();
  21. while (true){
  22. try {
  23. Thread.sleep(1000);
  24. }catch (Exception e){
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. public static void transferMoney(final Account fromAcct,
  30. final Account toAcct,
  31. final Long amount)
  32. throws Exception {
  33. class Helper {
  34. public void transfer() throws Exception {
  35. if (fromAcct.getAmount().compareTo(amount) < 0)
  36. throw new Exception("No enough money");
  37. else {
  38. fromAcct.debit(amount);
  39. toAcct.credit(amount);
  40. System.out.println(Thread.currentThread().getName()+"完成转账");
  41. }
  42. }
  43. }
  44. // 得到锁的hash值
  45. int fromHash = System.identityHashCode(fromAcct);
  46. int toHash = System.identityHashCode(toAcct);
  47. // 根据hash值来上锁 不管是汇款账户还是入款账户,总是Hash值小的先锁,则对象锁的顺序是固定的
  48. if (fromHash < toHash) {
  49. synchronized (fromAcct) {
  50. System.out.println(Thread.currentThread().getName()+"获取账户"+fromAcct.getName()+"锁");
  51. synchronized (toAcct) {
  52. System.out.println(Thread.currentThread().getName()+"获取账户"+toAcct.getName()+"锁");
  53. new Helper().transfer();
  54. }
  55. }
  56. } else if (fromHash > toHash) { // 根据hash值来上锁
  57. synchronized (toAcct) {
  58. System.out.println(Thread.currentThread().getName()+"获取账户"+toAcct.getName()+"锁");
  59. synchronized (fromAcct) {
  60. System.out.println(Thread.currentThread().getName()+"获取账户"+fromAcct.getName()+"锁");
  61. new Helper().transfer();
  62. }
  63. }
  64. } else { //如果是同对象,由于 synchronized 已经支持可重入锁,所以并发的同账户互相转账不会产生死锁,锁顺序不产生影响
  65. synchronized (fromAcct) {
  66. synchronized (toAcct) {
  67. new Helper().transfer();
  68. }
  69. }
  70. }
  71. }
  72. }

程序执行结果

  1. Thread-0获取账户A
  2. Thread-0获取账户B
  3. Thread-0完成转账
  4. Thread-1获取账户A
  5. Thread-1获取账户B
  6. Thread-1完成转账

如结果所示,并不会产生死锁。

协作锁之间发生死锁

除了动态顺序锁发生死锁的情况,还存在一些死锁的情况,但是不是顺序锁那么简单可以被发现。因为有可能并不是在同一个方法中显示请求两个锁,而是嵌套另一个方法去获取第二个锁。这就是隐式获取两个锁(对象之间协作)。

例如,ProductProducer生产者会不断的生产商品并且向仓库注册商品,ProductDepository商品仓库会不断的处理注册到仓库的商品加工,生成编号。

  1. public class CooperatingDeadlock {
  2. public static void main(String[] args) {
  3. ProductProducer productProducer = new ProductProducer();
  4. ProductDepository productDepository = new ProductDepository();
  5. productProducer.setProductDepository(productDepository);
  6. productProducer.start();
  7. productDepository.start();
  8. while (true){
  9. try {
  10. Thread.sleep(1000);
  11. }catch (Exception e){
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. class ProductProducer extends Thread{
  18. public void setProductDepository(ProductDepository productDepository) {
  19. this.productDepository = productDepository;
  20. }
  21. private ProductDepository productDepository;
  22. @Override
  23. public void run(){
  24. produce();
  25. }
  26. // produce()需要ProductProducer对象锁
  27. private synchronized void produce(){
  28. do {
  29. Product p = new Product(this);
  30. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,生产商品,并向仓库注册");
  31. this.productDepository.addAvailable(p);
  32. try{
  33. Thread.sleep(100);
  34. }catch (Exception e){
  35. e.printStackTrace();
  36. }
  37. }while (true);
  38. }
  39. public synchronized void handle(Product product) {
  40. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,加工商品,生成编号");
  41. product.setNo(System.currentTimeMillis());
  42. }
  43. }
  44. class ProductDepository extends Thread{
  45. public ProductDepository(){
  46. availableProductList = new ArrayList<>();
  47. }
  48. private ArrayList<Product> availableProductList;
  49. @Override
  50. public void run(){
  51. handleAvailable();
  52. }
  53. public synchronized void addAvailable(Product product) {
  54. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,注册到仓库");
  55. availableProductList.add(product);
  56. }
  57. // handleAvailable()需要ProductDepository对象锁
  58. public synchronized void handleAvailable() {
  59. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,将注册的商品进行加工");
  60. do{
  61. for (Product t : availableProductList)
  62. // 调用handle()需要ProductProducer对象锁
  63. t.handle();
  64. availableProductList.clear();
  65. try {
  66. Thread.sleep(100);
  67. } catch (InterruptedException e) {
  68. e.printStackTrace();
  69. }
  70. }while (true);
  71. }
  72. }
  73. class Product{
  74. private ProductProducer productProducer;
  75. public Product(){}
  76. public void setNo(Long no) {
  77. this.no = no;
  78. }
  79. private Long no;
  80. public Product(ProductProducer productProducer){
  81. this.productProducer = productProducer;
  82. }
  83. public void handle(){
  84. this.productProducer.handle(this);
  85. }
  86. }

此时我们执行程序:

  1. Thread-0获取com.pubutech.multithread.example.deadlock.ProductProducer对象锁,生产商品,并向仓库注册
  2. Thread-1获取com.pubutech.multithread.example.deadlock.ProductDepository对象锁,将注册的商品进行加工

很显然,我们的程序将陷入死锁无法继续执行下去。

开放调用避免死锁

在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!

如果在调用某个方法时不再持有锁,而改为同步代码块仅用于保护那些涉及共享状态的操作!那么这种调用被称为开放调用!

我们可以这样来改造:

  1. public class OpenMethodCooperationDeadLock {
  2. public static void main(String[] args) {
  3. OpenMethodProductProducer productProducer = new OpenMethodProductProducer();
  4. OpenMethodProductDepository productDepository = new OpenMethodProductDepository();
  5. productProducer.setProductDepository(productDepository);
  6. productProducer.start();
  7. productDepository.start();
  8. while (true){
  9. try {
  10. Thread.sleep(1000);
  11. }catch (Exception e){
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. class OpenMethodProductProducer extends Thread{
  18. public void setProductDepository(OpenMethodProductDepository productDepository) {
  19. this.productDepository = productDepository;
  20. }
  21. private OpenMethodProductDepository productDepository;
  22. @Override
  23. public void run(){
  24. produce();
  25. }
  26. private void produce(){
  27. do {
  28. OpenMethodProduct p = null;
  29. //需要ProductProducer对象锁,但是缩小锁的范围,不会同时去获取两个锁
  30. synchronized (this){
  31. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,生产商品,并向仓库注册");
  32. p = new OpenMethodProduct(this);
  33. }
  34. if (null != p){
  35. System.out.println(Thread.currentThread().getName()+"向仓库注册");
  36. this.productDepository.addAvailable(p);
  37. }
  38. try{
  39. Thread.sleep(100);
  40. }catch (Exception e){
  41. e.printStackTrace();
  42. }
  43. }while (true);
  44. }
  45. public synchronized void handle(OpenMethodProduct product) {
  46. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,加工商品,生成编号");
  47. product.setNo(System.currentTimeMillis());
  48. }
  49. }
  50. class OpenMethodProductDepository extends Thread{
  51. public OpenMethodProductDepository(){
  52. availableProductList = new ArrayList<>();
  53. }
  54. private ArrayList<OpenMethodProduct> availableProductList;
  55. @Override
  56. public void run(){
  57. handleAvailable();
  58. }
  59. public synchronized void addAvailable(OpenMethodProduct product) {
  60. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,注册到仓库");
  61. availableProductList.add(product);
  62. }
  63. public void handleAvailable() {
  64. do{
  65. ArrayList<OpenMethodProduct> availableProductListCopy;
  66. //需要ProductDepository对象锁,但是缩小锁的范围,不会同时去获取两个锁
  67. synchronized (this){
  68. System.out.println(Thread.currentThread().getName()+"获取"+getClass().getName()+"对象锁,准备将注册的商品进行加工");
  69. availableProductListCopy = new ArrayList<>(availableProductList);
  70. availableProductList.clear();
  71. }
  72. for (OpenMethodProduct t : availableProductListCopy)
  73. // 调用handle()需要ProductProducer对象锁
  74. t.handle();
  75. try {
  76. Thread.sleep(100);
  77. } catch (InterruptedException e) {
  78. e.printStackTrace();
  79. }
  80. }while (true);
  81. }
  82. }
  83. class OpenMethodProduct{
  84. private OpenMethodProductProducer productProducer;
  85. public OpenMethodProduct(){ }
  86. public void setNo(Long no) {
  87. this.no = no;
  88. }
  89. private Long no;
  90. public OpenMethodProduct(OpenMethodProductProducer productProducer){
  91. this.productProducer = productProducer;
  92. }
  93. public void handle(){
  94. this.productProducer.handle(this);
  95. }
  96. }

程序执行结果如下,将会不断的协作下去直至我们终端程序而不会产生死锁

  1. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  2. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  3. Thread-0向仓库注册
  4. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  5. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  6. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
  7. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  8. Thread-0向仓库注册
  9. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  10. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  11. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  12. Thread-0向仓库注册
  13. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  14. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
  15. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  16. Thread-0向仓库注册
  17. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  18. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  19. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
  20. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
  21. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  22. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  23. Thread-0向仓库注册
  24. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  25. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,准备将注册的商品进行加工
  26. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  27. Thread-0向仓库注册
  28. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库
  29. Thread-1获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,加工商品,生成编号
  30. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer对象锁,生产商品,并向仓库注册
  31. Thread-0向仓库注册
  32. Thread-0获取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository对象锁,注册到仓库

RetreenLock锁超时解决死锁

synchronized 关键字的锁是由Java虚拟机实现的,它无法显示获取锁超时,但是Java5以后Java RetreenLock提供了tryLock()方法来实现获取锁超时,我们在获取锁时可以使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

发表评论

表情:
评论列表 (有 0 条评论,485人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线

    死锁: 如果一个资源被一个线程占用,而且无法释放线程资源,导致其他线程无法访问这个字段,一直处于等待状态,这样就会形成 线程死锁。 例子: package com.

    相关 线

    / 死锁:二个线程同时锁住一个变量时。 锁住一个变量之后,尽快操作完成解锁,解锁之前不要再锁住其它变量,否则会互锁(死锁)。 /

    相关 线

    峨眉山月半轮秋,影入平羌江水流 Java线程的死锁一直都是经典的多线程问题;因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都不能继续执行; 示例代码:

    相关 线实例定位

    既然可以上锁,那么假如有2个线程,一个线程想先锁对象1,再锁对象2,恰好另外有一个线程先锁对象2,再锁对象1。在这个过程中,当线程1把对象1锁好以后,就想去锁对象2,但是不巧,

    相关 线

    同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。