基本概念
锁的定义:锁是一种同步机制,用于管理对共享资源的访问,确保同一时间只有一个线程能够访问该资源。
互斥与同步:互斥是指同一时刻只允许一个线程访问资源;同步是指多个线程按照一定的顺序访问资源
锁的特性(解决的问题)
1、原子性
一个操作或多个操作要么都执行,要么都不执行。
主要通过锁机制实现,也可以使用原子类。synchronized和Atomic相关类可实现原子性,volatile无法保证。
2、有序性
程序执行的顺序按照代码的先后顺序执行,避免指令重排。
指令重排是指JVM在编译或运行时调整代码顺序,以优化程序性能。这种优化是在不改变程序执行结果的前提下进行的。例如去采购ABC三样东西,jvm会基于性能考虑改变购买顺序,可能结果从A->B->C变为B->A->C。
再举个栗子,单例模式懒汉加载模式中双重检测锁,instance需要用volatile修饰就是为了避免指令重排序。创建单例对象的正常过程:
分配内存空间 -> 初始化对象 -> 创建引用指向堆中的对象
假设没有使用volatile,两个线程同时去获取单例,T1通过第一次判断获得锁正在创建单例对象,顺序可能是 分配内存空间 -> 创建引用指向堆中的对象 -> 初始化对象 ,执行到第二步,这时T2获取单例,此时instance已经不为null但是还没初始化对象,直接return单例就会有问题。
有序性可以通过synchronized、volatile保证。
3、可见性
一个线程修改共享变量时,其他线程能立即看到。
可见性可以通过synchronized、volatile保证。
4、可重入性
一个线程获取锁后,锁释放前,再次获取锁不需要竞争和阻塞可直接获取锁。java中的锁一般都是可重入锁。
锁的分类
java中的锁分类就两种:独占锁,即写锁;共享锁,即读锁。其他如乐观锁、悲观锁,公平锁、非公平锁则是从其他角度去区分。
独占锁(写锁):在任何时刻只有一个线程可以持有该锁。持有锁的线程可以读数据,也可以写数据。写数据的时候会阻止其他线程对数据的任何操作。
共享锁(读锁):在同一时刻可以有多个线程持有该锁,线程只能读数据。读数据的时候不会阻止其他线程对数据的读操作,但是会阻止写操作。
锁的本质
线程对共享资源同步状态的竞争。在这个过程中会有性能问题,原因大致为:
1、竞争同步状态通过调用系统内核的Mutex机制,用户态切换到内核态,无法优化。
2、线程的阻塞和唤醒也涉及上下文切换,可优化(cas,自旋锁)。
3、并行到串行的改变,无法优化。
性能优化
1、锁粒度的优化。编码过程中主动优化锁的粒度,编译器层面控制的锁消除和锁膨胀。
锁消除:编码时加锁,实际运行时无竞争,编译器消除锁。
锁膨胀:加锁粒度太小,频繁加锁和释放锁,编译器将锁的范围扩大。
2、无锁编程,乐观锁(版本号,比较与设置)。
3、锁升级。synchronized优化,无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
偏向锁:第一次获取锁记录线程id,该线程未释放锁之前再次获取琐时,通过比较锁记录中的线程id,锁偏向该线程,无需竞争与阻塞直接获取锁。一般是在只有一个线程在竞争共享资源时。
轻量级锁:通过自旋锁 + cas,不加锁完成锁的竞争。一般是在只有少量线程在竞争时。
重量级锁:切换到内核态,Mutex机制加锁。有大量线程在竞争资源。
锁的实现
1、synchronized
关键字,jvm层面的锁,可重入。灵活性较差,不可中断,代码块执行完后自动释放锁,非公平锁实现。性能方面有锁升级优化,条件等待使用Object的wait/notify机制。
2、lock
接口,jdk层面。基于AQS框架实现原理,有公平锁和非公平锁实现,可重入。灵活性强,有提供不加锁的tryLock方法判断是否获取到锁。可以中断等待,设置超时时间,手动释放锁,一般在finally块中释放,否则可能导致死锁。条件等待可以创建多个Condition,实现精确唤醒。
3、cas
Compare-And-Swap(CAS)字面意思:”比较并交换“ ,是一种非阻塞式并发控制技术,它主要用于解决多个线程同时访问同一个共享资源时可能出现的竞争条件问题。它是一种无锁的同步机制(乐观锁),可以在不使用锁的情况下实现数据的同步和并发控制。cas有三个操作数:内存值V、预期值A、新值B。在执行操作之前,先比较当前内存V中的值是否等于期望值A,如果相等,则执行修改值为B;如果不相等,则不执行修改操作,继续进行比较,直到内存V中的值与期望值A相等为止。这个过程中不会出现线程的阻塞和唤醒,因此可以提高系统的并发性能。
4、自旋锁
在获取锁时不断循环检查(耗cpu)锁是否可用的锁机制,空转重试而不是让线程进入阻塞状态。不适合长时间持锁的场景,可设置超时时间,重试次数。通过java.util.concurrent.atomic包下的原子变量类实现。
5、AQS框架
AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态。
维护一个volatile int state(同步状态),FIFO线程等待队列-双向队列,Condition等待队列:存储主动等待的线程(调用await的线程),基于CAS操作,自旋锁实现线程的竞争和等待。提供了公平锁、非公平锁的实现,默认非公平。
公平锁:锁释放后优先从等待队列中取出最早进入等待队列的线程分配锁。由于涉及到线程上下文切换,性能上来说有损耗。
非公平锁:锁释放后如果正好有线程来获取锁,不需要看等待队列是否有线程,直接尝试获取,成功则返回,失败进入等待队列。由于可能正好处于临界点获取到锁,不需要切换上下文,性能相对公平锁要好
6、Atomic类
是一组在java并发包(java.util.concurrent
)中提供的类,主要用于在多线程环境中实现线程安全的操作。这些类被称为原子类,因为它们提供了一种原子操作,即一旦操作开始,就不会被其他线程中断,从而确保操作的原子性。底层通过cas操作来实现原子性。