Java内部有两种锁机制

134 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情 >>

Java内部有两种锁机制:

1.synchonized 2.Lock

区别:

  • 实现机制不同
  • synchonrized 分为两种 程序段的synchonized是通过monitor.enter monitor.exit来实现的,方法和类级别的则是通过设置实例或者类的锁字段来实现
  • Lock的实现方式则是通过AQS。AQS是一个线程的链表,负责维护线程的状态,以及线程的调度,AQS也是一个锁 保证同一时间获取AQS锁的线程只有一个,也就是下面的Node status为runnning的只有一个(为什么不是同一时间运行的线程只有一个呢? 线程在申请锁的时候先加入队列然后挂起,并且在公平竞争时所有的线程都会别唤醒 )

synchronized 同步锁

  • 原理:任何一个对象都一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
  • MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。
  • 这时如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,需要消耗很多处理器时间。如果可能,应该减少这样的切换,jvm一般会采取一些措施进行优化,例如在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象

  • 静态同步方法,锁是当前类的class对象

  • 同步方法块,锁是括号里面的对象

  • javap工具查看生成的class文件信息来分析Synchronize的实现

  • 同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

  • 同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

  • Java对象头和monitor是实现synchronized的基础

    #特性:互斥锁、非公平锁、可重入、不可中断、使用简单

    #性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。

Lock锁实现:

ReentrantLock 重入锁

  • 使用:ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操作线程,await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似
  • 原理:核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
  • 特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的问题
  • 性能和建议:性能中等,建议需要手动操作线程时使用。

ReentrantReadWriteLock 读写锁

  • 使用:它允许多个线程读某个资源,但每次只允许一个线程来写。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。

  • 当多个线程读取有个变量时可以使用读锁rwl.readLock().lock();,如果需要去修改某个变量时则可以上写锁rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写

  • 与重入锁比较,其实现原理一致,但是读写锁更适合读多写少的场景,因为读读共享,而重入锁全互斥

StampedLock 时间戳锁(jdk1.8改进的读写锁)

  • 使用:写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写,在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
  • 时间戳锁与读写锁比较:读锁不阻塞写锁,如果时间戳无效,则重新读取变量值。无ABA问题。