JUC-第三讲之谈谈Java中的锁

55 阅读9分钟

推荐一个牛逼up:孙哥
孙哥主页

乐观锁和悲观锁

悲观锁

synchronized关键字和Lock的实现类都是悲观锁

特点:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

适合场景:写操作 插入/更改

乐观锁

特点:认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。

在Jva中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。 如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等

判断规则:

  1. 版本号机制Version
  2. 最常采用的是CAS算法,Java原子类的递增操作就通过CAS自旋实现的

适合场景:读操作 查询

8种情况演示锁的运行

锁相关的8种案例

题目:谈谈你对多线程锁的理解,8锁案例说明 口诀:线程 操作 资源类 8锁案例说明:

  1. 标准访问有āb两个线程,请问先打印邮件还是短扃
  2. sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
  3. 添加一个普通的hello.方法,清间先打印邮件还是eLLo
  4. 有两部手机,请间先打印邮件还是短信
  5. 有两个静态同步方法,有1部手机,请问先打印邮件还是短信
  6. 有两个静态同步方法,有2部手机,请问先打印邮件还是短信
  7. 有个静态同步方法,有1个普通同步方法有部手机,请问先打印邮件还是短局
  8. 有个静态同步方法,有1个普通同步方法有部手机,请问先打印邮件还是短信 :::info 总结: 1-2: 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized.方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯成一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法 3-4: 加个普通方法后发现和同步锁无关 换成两个对象后,不是同一把锁了,情况立刻变化。 5-6 三种synchronized锁的内容有一些差别: 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁一>实例对象本身, 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板 对于同步方法块,锁的是synchronized括号内的对象 7-8: 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异闹时必须释放锁。 所有的普通同步方法用的都是同一把锁一实例对象本身,就是new出来的具体实例对象本身,本类this 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。 所有的静态同步方法用的也是同一把锁一类对象本身,就是我们说过的唯一模板Class 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的 但是一且一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。 :::
class Phone{
   public  synchronized void sendEmail() throws InterruptedException {
      TimeUnit.SECONDS.sleep(3);
      System.out.println("send Email");
   }
   public static synchronized void sendSMS(){
      System.out.println("send SMS");
   }
   public void hello(){
      System.out.println("hello");
   }

}
public class Lock8Demo {
   public static void main(String[] args) throws InterruptedException {
      Phone phone = new Phone();
      Phone phone2=new Phone();
      new Thread(()->{
         try {
            phone.sendEmail();
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
      },"a").start();
      TimeUnit.MICROSECONDS.sleep(200);
      new Thread(()->{
         phone.sendSMS();
      },"b").start();
      new Thread(()->{
         phone2.hello();
      },"c").start();
   }
}

Synchronized三种应用方式

:::info 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁: 作用于代码块,对括号里配置的对象加锁。 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁; :::

从字节码角度分析synchronized实现

:::info javap-c*.class文件反编译 synchronized同步代码块 synchronized普通同步方法 synchronized静态同步方法 :::

public class LockSyncDemo {
    Object object = new Object();

    public LockSyncDemo() {
    }

    public void m1() {
        synchronized(this.object) {
            System.out.println("-----hello synchronized code3 block");
        }
    }

    public static void main(String[] args) {
    }
}

通过 javap -c .\LockSyncDemo.class

image.png

底层通过 monitorenter 和monitorexit 加锁和释放锁

image.png 一般情况 一个加锁 ,对应两个释放锁

public class LockSyncDemo {
        Object object=new Object();
//        public void  m1(){
//            synchronized (object){
//                System.out.println("-----hello synchronized code3 block");
//            }
//        }

    public synchronized void m2(){
        System.out.println("------------hello synchronized m2-");
    }

    public static void main(String[] args) {

    }
}

javap -v image.png

静态同步方法和普通同步方法对比

public class Lock8Demo {
   public static void main(String[] args) throws InterruptedException {
      Phone phone = new Phone();
      Phone phone2=new Phone();
      new Thread(()->{
         try {
            phone.sendEmail();
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
      },"a").start();
      TimeUnit.MICROSECONDS.sleep(200);
      new Thread(()->{
         phone.sendSMS();
      },"b").start();
      new Thread(()->{
         phone2.hello();
      },"c").start();
   }
}

image.png

反编译synchronized锁是什么?

为什么任何一个对象都可以成为一个锁呢? image.png image.png 每个对象都带着一个对象监视器,每个被锁住的对象都会和Monitor关联起来,也就是会 Moinitor 的owner会指向获得锁的线程

公平锁和非公平锁

class Ticket{
   public int number=50;
   ReentrantLock lock=new ReentrantLock();
   public void sale(){
      lock.lock();
      try {
            if(number>0){
               System.out.println(Thread.currentThread().getName()+"卖出去第:"+(number--)+"还剩下"+number);
            }
      }finally {
         lock.unlock();
      }

   }
}
public class SaleTicketDemo {
   public static void main(String[] args) {
      Ticket ticket=new Ticket();
      new Thread(()->{
         while (ticket.number>0) {
            ticket.sale();
         }
      },"a").start();
      new Thread(()->{
         while (ticket.number>0) {
            ticket.sale();
         }
      },"b").start();
      new Thread(()->{
         while (ticket.number>0) {
            ticket.sale();
         }
      },"c").start();

   }
}

ReentrantLock lock=new ReentrantLock(); 默认非公平锁 多个线程抢夺资源差别大 ReentrantLock lock=new ReentrantLock(true); 公平锁 :::info 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 Lock lock=new ReentrantLock(true);/true表示公平锁,先来先得 非公平锁 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程直得不到锁) Lock lock=new ReentrantLock(false);//false表示非公平锁,后来的也可能先获得锁 Lock lock=new ReentrantLock();/默认非公平锁 :::

面试题

为什么会有公平锁/非公平锁的设计?为什么默认非公平?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间:
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平锁,什么时候用非公平锁?

如果为了更高的吞吐量,很显然非公平锁是比较合的,因为节省很多线程切换时间,吞吐量自然就上去了; 否则那就用公平锁,大家公平使用。

可重入锁(又名递归锁)

:::info 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。 如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。 所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。 ::: image.png

synchronized 关键字使用的锁,默认就是可重入锁

public class ReEntryLockDemo {
   public static void main(String[] args) {
         final Object o =new  Object();
         new Thread(()->{
            synchronized (o){
               System.out.println(Thread.currentThread().getName()+"外层调用");
               synchronized (o){
                  System.out.println(Thread.currentThread().getName()+"中层调用");
                  synchronized (o){
                     System.out.println(Thread.currentThread().getName()+"内存调用");
                  }
               }
            }
         },"t1").start();
   }
}

:::info t1外层调用 t1中层调用 t1内存调用 :::

public class ReEntryLockDemo {
    public synchronized void m1(){
        System.out.printf(Thread.currentThread().getName()+"----come in");
        m2();
    }
    public synchronized void m2(){
        System.out.printf(Thread.currentThread().getName()+"----come in");
        m3();
    }
    public synchronized void m3(){
        System.out.printf(Thread.currentThread().getName()+"----come in");
    }

   public static void main(String[] args) {
         final Object o =new  Object();
         ReEntryLockDemo reEntryLockDemo=new ReEntryLockDemo();
         new Thread(()->{
             reEntryLockDemo.m1();
         },"t1").start();
//         new Thread(()->{
//            synchronized (o){
//               System.out.println(Thread.currentThread().getName()+"外层调用");
//               synchronized (o){
//                  System.out.println(Thread.currentThread().getName()+"中层调用");
//                  synchronized (o){
//                     System.out.println(Thread.currentThread().getName()+"内存调用");
//                  }
//               }
//            }
//         },"t1").start();
   }
}

:::info t1----come int1----come int1----come in :::

可重入锁的实现原理

:::info 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。 当执行monitorenterl时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。 当执行monitorexitl时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。 :::

显示锁 即ReentrantLock

       Lock lock=new ReentrantLock();
         new Thread(()->{
            lock.lock();
               System.out.println(Thread.currentThread().getName()+"外层调用");
              lock.lock();
                  System.out.println(Thread.currentThread().getName()+"中层调用");
                 lock.lock();
                     System.out.println(Thread.currentThread().getName()+"内存调用");
            lock.unlock();
            lock.unlock();
            lock.unlock();

         },"t1").start();

死锁以及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

public class Demo {
   static Object o1=new Object();
   static Object o2=new Object();
   static   void m1() throws InterruptedException {
      synchronized (o1){
         System.out.println("m1拿到了o1");
         Thread.sleep(1000);
         synchronized (o2){
            System.out.printf("m1拿到了o2");
         }
      }
   }
   static void m2() throws InterruptedException {
      synchronized (o2){
         System.out.println("m2拿到了o2");
         Thread.sleep(1000);
         synchronized (o1){
            System.out.printf("m2拿到了o1");
         }
      }
   }
   public static void main(String[] args) {
       new Thread(()->{
          try {
             m1();
          } catch (InterruptedException e) {
             throw new RuntimeException(e);
          }
       },"t1").start();
      new Thread(()->{
         try {
            m2();
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
      },"t2").start();

   }
}

image.png

如何排查死锁

jps -l jstack 12692(进程编号) image.png image.png 发现一个死锁