当前位置: 首页 > 科技观察

Java中的锁《原理、锁优化、CAS、AQS》

时间:2023-03-12 06:16:41 科技观察

1.为什么要用锁?锁是为了解决并发操作导致的脏读和数据不一致的问题。二、锁实现的基本原理2.1、volatileJava编程语言允许线程访问共享变量。为了保证共享变量能够准确一致的更新,线程要保证这个变量是通过独占锁单独获取的。Java语言提供了volatile,在某些情况下比锁更方便。volatile保证共享变量在多处理器开发中的“可见性”。可见性意味着当一个线程修改共享变量时,另一个线程可以读取修改后的值。结论:如果volatile变量修饰符使用得当,使用和执行比synchronized更便宜,因为它不会引起线程上下文切换和调度。2.2、synchronizedsynchronized通过锁机制实现同步。我们先来看一下使用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下三种形式。对于普通的同步方法,锁是当前实例对象。对于静态同步方法,锁是当前类的Class对象。对于一个synchronized方法块,锁就是Synchonized括号中配置的对象。当一个线程试图访问一个同步代码块时,它必须首先获取锁并在退出或抛出异常时释放锁。2.2.1synchronized实现原理Synchronized是基于Monitor实现同步的。Monitor从两个方面支持线程间的同步:互斥与协作1、Java使用对象锁(使用synchronized获取对象锁)来保证工作在共享数据集上的线程是互斥的。2、使用notify/notifyAll/wait方法来协调不同线程之间的工作。3.Class和Object都关联了一个Monitor。Monitor的工作机制线程进入同步方法。为了继续执行临界区代码,线程必须获得Monitor锁。如果锁获取成功,就会成为monitor对象的拥有者。在任何时候,监视器对象只属于一个活动线程(TheOwner)。拥有监视器对象的线程可以调用wait()进入等待集(WaitSet),同时释放监视器锁,进入等待状态。其他线程调用notify()/notifyAll()接口唤醒等待集中的线程。这些等待线程在执行wait()之后的代码之前需要重新获取监听锁。同步方法执行完成后,线程退出临界区,释放监控锁。参考文档:https://www.ibm.com/developerworks/cn/java/j-lo-synchronized2.2.2Synchronized具体实现1.同步代码块由monitorenter和monitorexit指令显式实现。2.同步方法是使用ACC_SYNCHRONIZED标签隐式实现的。下面通过一个例子来看看具体实现:javap编译后的字节码如下:monitorenter每个对象都有一个监视器,一个监视器只能由一个线程拥有。当一个线程执行monitorenter指令时,它会尝试获取对应对象的monitor。获取规则如下:如果monitor表项数为0,则线程可以进入monitor,如果设置monitor表项数为1,则线程为monitor。所有者。如果当前线程已经拥有monitor,刚刚重新进入,进入monitor的次数会加1,所以synchronized关键字实现的锁是可重入锁。如果监视器已经被其他线程拥有,则当前线程进入阻塞状态,直到监视器条目数为0,然后再次尝试获取监视器。monitorexit只有拥有相应对象的监视器的线程才能执行monitorexit指令。这条指令每执行一次,monitorentry数量减1,当entry数量为0时,当前线程释放monitor。这时候其他阻塞的线程就可以尝试获取监听器了。2.2.3锁的存放位置锁标志存放在Java对象头的MarkWord中。Java对象头长度32位JVMMarkWord结构32位JVMMarkWord状态变化64位JVMMarkWord结构2.2.3同步锁优化JavaSE1.6引入了“偏向锁”和“轻量级锁”。在JavaSE1.6中,锁有四种状态,级别从低到高依次为:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这些状态会随着比赛形势的变化而逐渐发生变化。升级。锁可以升级,不能降级,也就是说偏向锁升级为轻量级锁后,不能再降级为偏向锁。这种锁升级不降级策略的目的是为了提高获取和释放锁的效率。偏向锁:在没有锁竞争的情况下,为了减少锁竞争的资源开销,引入偏向锁。轻量级锁:轻量级锁适用的场景是线程交替执行同步块的情况。锁粗化(LockCoarsening):即减少不必要的紧密相连的解锁和加锁操作,将多个连续的锁扩展成一个更大的锁。LockElimination:LockElimination指的是在虚拟机即时编译器运行时,在某些代码上需要同步的锁被消除,但是检测到没有共享数据竞争。AdaptiveSpinning:自适应是指自旋时间不再是固定的,而是由同一把锁上的前一次自旋时间和锁拥有者的状态决定的。如果在同一个锁对象上,自旋等待刚刚成功获取到锁,持有锁的线程正在运行,那么虚拟机会认为这次自旋很可能再次成功,然后会允许自旋等待相对较长的时间,比如100次循环。另一方面,如果对于一个锁来说,自旋很少能成功获取,那么在以后获取锁的时候可以省略自旋过程,避免处理器资源的浪费。2.2.4锁的优缺点比较2.3、CASCAS,在Java并发应用中通常指的是CompareAndSwap或CompareAndSet,即比较和交换。1.CAS是一个原子操作。它比较一个内存位置的值,只有在相等时才将这个内存位置的值修改为新值。它确保始终根据最新信息计算新值。如果这里有其他线程在此期间修改了这个值,CAS就会失败。CAS返回是否成功或以内存位置的原始值来判断CAS是否成功。2、JVM中的CAS操作是使用处理器提供的CMPXCHG指令实现的。优点:竞争不多时系统开销小。缺点:周期长,开销大。ABA问题。原子操作只能保证一个共享变量。3.Java3.1中的锁实现。QueueSynchronizer(AQS)QueueSynchronizerAbstractQueuedSynchronizer(以下简称同步器)是构建锁或其他同步组件的基础框架。3.1.1.它使用一个int成员变量来表示同步状态。3.1.2.使用内置的FIFO双向队列完成锁获取线程的排队工作。同步器包含两个节点类型的应用程序,一个指向头节点,一个指向尾节点。没有获取到锁的线程会创建一个线程安全的节点(compareAndSetTail)加入队列的尾部。同步队列遵循FIFO,第一个节点是成功获得同步状态的节点。没有获得锁的线程会创建一个节点,并将其设置为结束节点。如下图所示:当第一个节点的线程释放锁时,会唤醒后继节点。成功获取锁后,后继节点会将自己设置为第一个节点。如下图所示:3.1.3Exclusive/sharedlockacquireExclusive:只有一个线程可以获得锁,如:ReentrantLock;Shared:多个线程可以同时获取锁,如:CountDownLatch;Exclusive公式中的每个节点自旋,看它的前一个节点是否是Header节点,如果是,则尝试获取锁。独占锁获取过程:共享:共享和独占的区别:共享锁获取过程:4、锁的用例4.1、ConcurrentHashMap的实现原理及ConcurrentHashMap类图ConcurrentHashMap数据结构的使用结语:ConcurrentHashMap技术使用的锁切分.首先将数据分块逐条存储,然后为每段数据分配一把锁。当一个线程占用锁访问一段数据时,其他段数据也可以被其他线程访问。