线程的相关锁

线程的相关锁

死锁

死锁定义

1)进程死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2)线程死锁是指由于两个或者两个以上的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

产生死锁的四个必要条件

1**)互斥条件:**指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程(或线程,以下同)占用。如果此时还有其它进程(线程)请求资源,则请求者只能等待,直至占有资源的进程(线程)用毕释放。如:当线程A进入synchronized,持有一个对象(Monitor)后,其他线程不能再持有这个对象了。

2**)请求和保持条件:**指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3**)不剥夺条件:**指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4**)环路等待条件:**指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

package com.xxgc.Lock;

//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
   public static void main(String[] args) {
       Makeup girl1 = new Makeup(0,"灰姑凉");
       Makeup girl2 = new Makeup(1,"白雪公主");

       girl1.start();
       girl2.start();
  }
}

//口红类
class Lipstick{

}

//镜子类
class Mirror{

}

class Makeup extends Thread {
   //需要的资源只有一份,用static来保证只有一份
   static Lipstick lipstick = new Lipstick();
   static Mirror mirror = new Mirror();

   int choice; //选择
   String girlName; //使用化妆品的女孩

   Makeup(int choice, String girlName) {
       this.choice = choice;
       this.girlName = girlName;
  }

   @Override
   public void run() {
       //化妆
       try {
           makeup();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
  }

   //化妆:互相持有对方的锁,就是需要拿到对方的锁
   private void makeup() throws InterruptedException {
       if (choice == 0) {
           synchronized (lipstick) { //获得口红的锁
               System.out.println(this.girlName + "获得口红的锁");
               Thread.sleep(1000);
               synchronized (mirror) {  //1s钟后想获得镜子
                   System.out.println(this.girlName + "获得镜子的锁");
              }
          }
      } else {
           synchronized (mirror) {  //获得镜子的锁
               System.out.println(this.girlName + "获得镜子的锁");
               Thread.sleep(2000);
               synchronized (lipstick) {  //2s钟后想获得口红
                   System.out.println(this.girlName + "获得镜子的锁");
              }
          }
      }
  }

}
  • 上面产生了死锁问题

  • 解决:将锁拿到外面,只要两人不要抱同一把锁就行

  • 最好是线程每次只锁定一个对象并且在锁定该对象的过程中不再去锁定其他的对象,这样就不会导致死锁了

package com.xxgc.Lock;

//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
   public static void main(String[] args) {
       Makeup girl1 = new Makeup(0,"灰姑凉");
       Makeup girl2 = new Makeup(1,"白雪公主");

       girl1.start();
       girl2.start();
  }
}

//口红类
class Lipstick{

}

//镜子类
class Mirror{

}

class Makeup extends Thread {
   //需要的资源只有一份,用static来保证只有一份
   static Lipstick lipstick = new Lipstick();
   static Mirror mirror = new Mirror();

   int choice; //选择
   String girlName; //使用化妆品的女孩

   Makeup(int choice, String girlName) {
       this.choice = choice;
       this.girlName = girlName;
  }

   @Override
   public void run() {
       //化妆
       try {
           makeup();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
  }

   //化妆:互相持有对方的锁,就是需要拿到对方的锁
   private void makeup() throws InterruptedException {
       if (choice == 0) {
           synchronized (lipstick) { //获得口红的锁
               System.out.println(this.girlName + "获得口红的锁");
               Thread.sleep(1000);
          }
           //将锁拿到外面,只要两人不要抱同一把锁就行
           synchronized (mirror) {  //1s钟后想获得镜子
               System.out.println(this.girlName + "获得镜子的锁");
          }
      } else {
           synchronized (mirror) {  //获得镜子的锁
               System.out.println(this.girlName + "获得镜子的锁");
               Thread.sleep(2000);
          }
           synchronized (lipstick) {  //2s钟后想获得口红
               System.out.println(this.girlName + "获得镜子的锁");
          }
      }
  }

}

总结:死锁常见于,线程在锁定对象还没释放时,又需要锁定另一个对象,并且此时该对象可能被另一个线程锁定。这种时候很容易导致死锁。因此在开发时需要慎重使用锁,尤其是需要注意尽量不要在锁里又加锁。

Lock锁

一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的)

img

  (1)Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。

    Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。 ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。   (2)Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。


二:synchronized的缺陷

  synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

   synchronized 的局限性 与 Lock 的优点 

  如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  1:占有锁的线程执行完了该代码块,然后释放对锁的占有;

  2:占有锁线程执行发生异常,此时JVM会让线程自动释放锁;

  3:占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

  试考虑以下三种情况: 

Case 1 :

  在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

Case 2 :

  我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

Case 3 :

  我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。

package com.xxgc.Lock;

import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock {
   public static void main(String[] args) {
       TestLock2 testLock2 = new TestLock2();

       new Thread(testLock2).start();
       new Thread(testLock2).start();
       new Thread(testLock2).start();
  }
}
class TestLock2 implements Runnable{
   //票数
   int ticketNums = 10;

   //定义Lock锁
   private final ReentrantLock lock = new ReentrantLock();
   @Override
   public void run() {
       while (true){
           try {
               lock.lock(); //加锁
               if (ticketNums>0){
                   try {
                       Thread.sleep(1000);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
                   System.out.println(ticketNums--);
              }else {
                   break;
              }
          }finally {
               //解锁
               lock.unlock();
          }
      }
  }
}

总结:不管是synchronized关键字还是Lock锁,都是用来在多线程的环境下对资源的同步访问进行控制,用以避免因多个线程对数据的并发读写造成的数据混乱问题。与synchronized不同的是,Lock锁实现同步时需要使用者手动控制锁的获取和释放,其灵活性使得可以实现更复杂的多线程同步和更高的性能,但同时,使用者一定要在获取锁后及时捕获代码运行过程中的异常并在finally代码块中释放锁。

 

 

posted @ 2020-09-20 02:37  墨染念颖  阅读(164)  评论(0)    收藏  举报