Java锁

Java锁

悲观锁

概述

  1. 认为自己在使用数据的时候一定有别的线程跟修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  2. synchronized关键字和Lock的实现类都是悲观锁

适用场景

适合写操作多的场景,先加锁可以保证写操作时数据正确。

乐观锁

概述

认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。

  • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
  • 如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。

适用场景

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

实现方式

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

8锁案例

阿里巴巴代码规范

  • 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

  • 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

8锁演示

class Phone{

    public synchronized void sendEmail(){

        try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("-----sendEmail");
    }

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

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

/**
 * - 题目:谈谈你对多线程锁的理解,8锁案例说明
 * - 口诀:线程 操作 资源类
 * 1. 标准访问有ab两个线程,请问先打印邮件还是短信? 答:先sendEmail
 * 2. a里面故意停3秒?答:先sendEmail
 * 3. 添加一个普通的hello方法,请问先打印邮件还是hello?答:先hello
 * 4. 有两部手机,请问先打印邮件(这里有个3秒延迟)还是短信? 答:先sendSMS
 * 5. 有两个静态同步方法(synchronized 前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?答:先sendEmail
 * 6. 两个手机,有两个静态同步方法(synchronized 前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?答:先sendEmail
 * 7. 一个静态同步方法,一个普通同步方法,请问先打印邮件还是短信?答:先sendSMS
 * 8. 两个手机,一个静态同步方法,一个普通同步方法,请问先打印邮件还是短信?答:先sendSMS
 */
public class LockDemo {

    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(()->{
            phone2.sendSMS();
        },"b").start();
    }
}

8锁原理

  • 1、2

    • 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法
  • 3

    • 普通方法不会和同步方法去竞争
  • 4

    • 普通同步方法锁的是对象,第四题中提到的是两部手机,所以有两个资源对象不会竞争
  • 5、6

    • 对于静态同步方法,锁的是当前类的 Class对象,如Phone.class唯一的个模板付于同步方法块,锁的是 synchronized 括号内的对象
    • 对于普通同步方法,锁的是当前实例对象,通常指this, 具体的一部手机,所有前普通同步方法用的都是同一把锁一>实例对象本身
  • 7、8

    • 两把不同的锁,一个是对象锁,一个是类锁,所以不会产生竞争关系

8锁案例的体现

  • 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁。

  • 作用于代码块,对括号里配置的对象加锁。

  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

字节码角度分析synchronized实现

文件反编译技巧

  • 文件反编译javap -c ***.class文件反编译,-c表示对代码进行反汇编
  • 假如需要更多信息 javap -v ***.class ,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)

synchronized 同步代码块

public class SynchronizedDemo {

    final Object object = new Object();

    public void test(){
        synchronized (object){
            System.out.println("-----hello synchronized code block");
        }
    }

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

反编译

从target中找到SynchronizedDemo.class文件,右键,open in terminal,然后javap -c SynchronizedDemo.class

Compiled from "SynchronizedDemo.java"
public class com.zjh.java.SynchronizedDemo {
  final java.lang.Object object;

  public com.zjh.java.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #2                  // class java/lang/Object
       8: dup
       9: invokespecial #1                  // Method java/lang/Object."<init>":()V
      12: putfield      #3                  // Field object:Ljava/lang/Object;
      15: return

  public void test();
    Code:
       0: aload_0
       1: getfield      #3                  // Field object:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter                      // 监视器,获得并进入
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String -----hello synchronized code block
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit                       // 正常情况下的释放锁
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit                       // 异常情况下的释放锁
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any

  public static void main(java.lang.String[]);
    Code:
       0: return
}

总结

  • synchronized同步代码块,实现使用的是moniterentermoniterexit指令(moniterexit可能有两个)
  • 那一定是一个moniterenter两个moniterexit吗?(不一定,如果主动去抛出一个异常,发现一个moniterenter,一个moniterexit,还有两个throw)

synchronized 普通同步方法

public class SynchronizedDemo {

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

    public static void main(String[] args) {

    }
}

反编译

类似于上述操作,最后调用javap -v SynchronizedDemo.class, -v:加上附加信息

public synchronized void test();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String ------hello synchronized m2
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 9: 0
      line 10: 8
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       9     0  this   Lcom/zjh/java/SynchronizedDemo;

总结

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitore然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

synchronized 静态同步方法

public class SynchronizedDemo {

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

    public static synchronized void test1(){
        System.out.println("------hello synchronized test---static");
    }

    public static void main(String[] args) {

    }
}

反编译

public static synchronized void test1();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=0, args_size=0
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #5                  // String ------hello synchronized test---static
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 13: 0
      line 14: 8

总结

ACC_STATIC, ACC_SYNCHRONIZED:静态同步方法
ACC_SYNCHRONIZED:普通同步方法

反编译synchronized锁的是什么

管程

  • 管程(英语: Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共字资源一般是硬件设备或一群变量。对共字变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个了程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

  • 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

为什么任何一个对象都可以成为一个锁?

  • Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。

  • 在HotSpot虚拟机中,monitor采用objectMonitor实现:ObjectMonitor.java → ObjectMonitor.cpp → objectMonitor.hpp

  • 每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来

// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
  _header       = NULL;
  _count        = 0;         // 用来记录该线程获取锁的次数
  _waiters      = 0,
  _recursions   = 0;         // 锁的重入次数
  _object       = NULL;
  _owner        = NULL;      // 指向持有ObjectMonitor对象的线程
  _WaitSet      = NULL;      // 存放处于wait状态的线程队列
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;     // 存放处于等待锁block状态的线程队列
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

公平锁和非公平锁

ReentrantLock 抢票案例

class Ticket {
    private int number = 30;
    ReentrantLock lock = new ReentrantLock(true);

    public void sale()
    {
        lock.lock();
        try {
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
/**
 * @author zjh
 */
public class SaleTicketDemo {

    public static void main(String[] args) {
        
        Ticket ticket = new Ticket();
        
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"a").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"b").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"c").start();
    }
}

公平锁

  • ReentrantLock lock = new ReentrantLock(true); true:公平锁 先来先得,默认不填是非公平

  • 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的

非公平锁

  • ReentrantLock lock = new ReentrantLock(); 默认就是非公平锁,false也是非公平锁

  • 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)

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

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。

  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程切换的开销。

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

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

可重入锁

可重入锁(又名递归锁)是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是一个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

"可重入锁" 详细解释

  • 可:可以

  • 重:再次

  • 入:进入

  • 锁:同步锁

  • 进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)

  • 一句话:一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入。自己可以获取自己的内部锁。

隐式锁 synchronized

synchronized是java中的关键字,默认是可重入锁,即隐式锁

  • 在同步块中
public class ReEntryLockDemo {

    public static void main(String[] args)
    {
        final Object objectLockA = new Object();

        new Thread(() -> {
            synchronized (objectLockA)
            {
                System.out.println("-----外层调用");
                synchronized (objectLockA)
                {
                    System.out.println("-----中层调用");
                    synchronized (objectLockA)
                    {
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}
  • 在同步方法中
public class ReEntryLockDemo {

    public synchronized void m1() {
        //指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
        System.out.println(Thread.currentThread().getName()+"\t"+"-----come in m1");
        m2();
        System.out.println(Thread.currentThread().getName()+"\t-----end m1");
    }
    
    public synchronized void m2() {
        System.out.println("-----m2");
        m3();
    }
    
    public synchronized void m3() {
        System.out.println("-----m3");
    }


    public static void main(String[] args) {

        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
        reEntryLockDemo.m1();
    }
}

synchronized 的重入实现机理

ObjectMoitor.hpp 文件

// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
  _header       = NULL;
  _count        = 0;         // 用来记录该线程获取锁的次数
  _waiters      = 0,
  _recursions   = 0;         // 锁的重入次数
  _object       = NULL;
  _owner        = NULL;      // 指向持有ObjectMonitor对象的线程
  _WaitSet      = NULL;      // 存放处于wait状态的线程队列
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;     // 存放处于等待锁block状态的线程队列
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}
  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

重入的过程

  1. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1

  2. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1
    否则需要等待直至持有线程释放该锁。

  3. 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁Lock

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁
  • lockunlock 要匹配
public class ReentrantLockDemo {

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
    }
}

// 结果
t1	----come in 外层调用
t1	------come in 内层调用
  • lockunlock 如果不匹配单线程下看不出什么问题,但是多线程下就会出现严重阻塞问题
public class ReentrantLockDemo {

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                // lock.unlock(); // 使lock与unlock次数不匹配
            }
        }, "t1").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("t2 ----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"t2").start();
    }
}

// 结果
t1	----come in 外层调用
t1	------come in 内层调用

注意:加锁几次就必须解锁几次,如果不解锁就会造成阻塞的状态程序无法运行。

死锁及排查

概述

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

产生死锁原因

  • 系统资源不足

  • 进程运行推进的顺序不合适

  • 资源分配不当

死锁代码案列

public class DeadLockDemo {

    public static void main(String[] args) {
        Object objectA = new Object();
        Object objectB = new Object();

        new Thread(()->{
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName()+"\t 持有a锁,想获得b锁");
                try {
                    TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+"\t 成功获得b锁");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (objectB){
                System.out.println(Thread.currentThread().getName()+"\t 持有b锁,想获得a锁");
                synchronized (objectA){
                    System.out.println(Thread.currentThread().getName()+"\t 成功获得a锁");
                }
            }
        },"B").start();
    }
}

如何排查死锁

命令检查

  • 第一步:jps -l 查看当前进程运行状况

  • 第二步:jstack 进程编号 查看该进程信息

图形检查

  • win + r 输入jconsole ,打开图形化工具,选择指定线程 ,点击左下角检测死锁
posted @ 2023-01-30 11:09  橙香五花肉  阅读(30)  评论(0编辑  收藏  举报