java中的锁

文章目录

  • java中的锁
  • 乐观锁和悲观锁
  • 读锁(共享锁)、写锁(排他锁)
  • 自旋锁、非自旋锁
  • 显式锁、隐式锁
  • 可重入锁、非重入锁
  • 如何实现互斥锁(mutex)?
  • Lock和synchronized的区别
  • ReentrantLock 和synchronized的区别
  • Synchronized的优化
  • Lock底层原理

  • volatile的实现原理
  • CAS的原子性操作如何保证
  • ThreadLocal
  • Thread的核心机制
  • 问题
  • 如何避免泄漏


乐观锁和悲观锁

根据线程是否要锁住同步资源,分为悲观锁(锁)和乐观锁(不锁)

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

锁实现方式:关键字 Synchornized,接口 Lock 的实现类

适用场景:写操作较多

乐观锁:认为自己再使用数据的时候不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候判断之前有没有别的线程更新了这个数据。

锁实现方式:CSA 算法,例如 AtomicInteger 类的原子自增是通过 CAS 自旋(如果内存中的版本与该线程中复制到的版本号不同时,会自旋,自旋是重新去读内存中的信息到线程中,再进行修改操作,操作完再尝试更新)实现的

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

读锁(共享锁)、写锁(排他锁)

读锁(共享锁)(用数据库来举例)

针对同一份数据,多个读操作可以同时进行而不互相影响。

当一个进程对表加了读锁后:该进程和其他进程都可对该表进行读操作; 该进程不能对表进行修改会产生 error;

该进程在释放该表的读锁前也不能读取其他的表;其他进程想对该表进行修改时,会进入阻塞状态,当锁释放后完成修改。

写锁(排它锁)

当写操作没有完成前,会阻断其他写锁和读锁。进程能够读自己上写锁的表;

进程能够写自己上写锁的表;该进程在释放该表的写锁之前不能读取其他表;
其他进程要读这个上了写锁的表,会进入阻塞状态,等锁释放后,完成读操作。

读写锁:ReentrantReadWriteLock lock= new ReentrantReadWriteLock();

读写锁下面分读锁和写锁,进行写操作可以上写锁:

lock.writeLock() 进行读操作可以上读锁:
lock.readLock()(读不加锁的话可能会产生脏读这些问题)

自旋锁、非自旋锁

自旋锁:当一个线程在获取锁的过程中,发现锁已经被其他线程获取,那么该线程循环等待,然后不断等待该锁是否能够被成功获取,自旋知道获取到锁才会退出。

自旋锁的意义及使用场景:

因为阻塞与唤醒需要操作系统切换 cpu 状态(涉及到上下文切换),需要消耗一定时间。有时自旋的时间比阻塞唤醒所需要的时间还短

自旋锁:固定次数自旋。自旋次数完成后还没有拿到锁,就认为更新失败

自适应自旋锁:假设不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。

JDK1.6 中 可 以 通 过 -XX : -UseSpining 参 数 关 闭 自 旋 锁 优 化 , - XX:PreBlockSpin 参数修改默认的自旋次数

JDK1.7 之后自旋锁的参数被取消,虚拟机不再支持用户配置自旋锁,自旋锁总是会被执行,并且自旋次数也由虚拟机自动调整。

显式锁、隐式锁

隐式锁,synchronized 是基于 jvm 的内置锁,加锁与解锁的过程不需要我们在代码中人为控制,jvm 会自动去加锁和解锁

显式锁,整个加锁跟解锁过程需要手动编写代码去控制,例如 ReentrantLock

可重入锁、非重入锁

可重入锁一个线程已经获得某个锁,可以再次获取锁而不会出现死锁。就是可以重复获取相同的锁。

只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。在锁设计时,不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。

不可重入锁当 A 方法获取 lock 锁去锁住一段需要做原子性操作的 B 方法时,如果这段 B 方法又需要

锁去做原子性操作,那么 A 方法就必定要与 B 方法出现死锁。这种会出现问题的重入一把锁的情况,叫不可重入锁。在锁设计时, 只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。

如何实现互斥锁(mutex)?

在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference。如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

自JDK 5起,Java类库中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。

Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  1. Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
  2. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
  3. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  4. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  5. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  6. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

ReentrantLock 和synchronized的区别

ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

划重点

相同点:

  1. 主要解决共享变量如何安全访问的问题
  2. 都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
  3. 保证了线程安全的两大特性:可见性、原子性。

不同点:

  1. ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
  2. ReentrantLock如果获取时间过长会自动释放,synchronized获取不到锁会一直等待
  3. ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
  4. ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
  5. ReentrantLock 通过 Condition 可以绑定多个条件
  6. synchronized适合于并发低的情况,因为synchronized存在锁升级,如果升级为重量级锁将会持续向cpu申请锁资源;ReentrantLock提供了阻塞队列,在高并发的情况下挂起,减少竞争,提高并发能力

Synchronized的优化

引入了锁升级机制、自旋锁和自适应自旋、锁消除、锁粗化

自旋锁与自适应自旋

在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除

虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

锁粗化

当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如StringBuffer 的 append 操作。

Lock底层原理

Lock的实现是基于AQS实现的,

AQS 使用一个被volatile修饰的 int 类型state变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

状态信息通过 protected 类型的getState()setState()compareAndSetState() 进行操作

//返回同步状态的当前值
protected final int getState() {
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock又可分为公平锁和非公平锁:
  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如 CountDownLatchSemaphoreCyclicBarrierReadWriteLock 我们都会在后面讲到。

所有通过AQS实现功能的类都是通过修改state的状态来操作线程的同步状态。比如在ReentrantLock中,一个锁中只有一个state状态,当state为0时,代表所有线程没有获取锁,当state为1时,代表有线程获取到了锁。通过是否能把state从0设置成1,当然,设置的方式是使用CAS设置,代表一个线程是否获取锁成功。

volatile的实现原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

CAS的原子性操作如何保证

是利用CPU原语来实现的,java的方法无法直接访问底层的系统,需要通过native方法来访问,Unsafe类里面的所有CAS方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务,JVM会帮助我们实现出CAS的汇编指令,这是完全依赖于硬件的功能,在实行的过程中不允许被中断,所以CAS是原子操作

ThreadLocal

Thread的核心机制

每个Thread线程内部都有一个Map。Map里面存储线程本地对象(key)和线程的变量副本(value),但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。

如何避免泄漏

为了防止此类情况的出现,我们有两种手段。

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。