在JAVA中使用锁来保证程序操作临界资源的互斥性,使多线程在竟态条件下对临界资源合理准确的操作。
1、锁的种类
序号 | 锁策略 | 应用 |
1 | 乐观锁 | CAS |
2 | 悲观锁 | synchronized、vector、hashtable |
3 | 自旋锁 | CAS |
4 | 可重入锁 | synchronized、Reentrantlock、Lock |
5 | 读写锁 | ReentrantReadWriteLock、CopyonWriteArrayList、CopyonWriteSet |
6 | 公平锁 | Reentrantlock(true) |
7 | 非公平锁 | synchronized、Reentrantlock(false) |
8 | 共享锁 | ReentrantReadWriteLock中的读锁 |
9 | 独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中的写锁 |
10 | 重量级锁 | synchronized |
11 | 轻量级锁 | 锁优化技术 |
12 | 偏向锁 | 锁优化技术 |
13 | 分段锁 | concurrentHashMap |
14 | 互斥锁 | synchronized |
2、CAS自旋锁
CAS:compare and swap,比较并交换
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
以上伪代码可以表示CAS的核心原理:CAS()函数中,比较&address
和expectedValue
是用于判断当前内存地址是否与期望值相等,如果相等就表示当前内存地址的值没有被其他线程修改,可以执行交换操作,即将新的值赋给内存地址。否则,如果值不相等,说明内存地址的值已经被其他线程修改,不执行修改操作。重新尝试整个CAS操作,直到成功为止。
2.1、CAS的ABA问题
ABA分别代表预期值的三种状态。
CAS的ABA状态可能会带来的问题:我们看一个具体的场景
我的账户里面有2000块钱(状态A),我委托张三说:如果我忘给李四转1000块钱,下午帮我转一下,我在中午给李四转了1000块钱(状态B),但是随后公司发奖金1000到我的账户,此时我账户有1000块钱(状态A),张三下午检查我账户,发现我有2000块钱,于是又给李四转了1000块钱,此时就出现问题了,李四收到了两次1000元,不符合我们的需求了。
解决ABA问题:
给预期值加一个版本号,在做CAS操作时,同时更新预期值的版本号,版本号只增不减,在进行CAS比较的时候,不仅预期值要相同,而且版本号也要相同,这个时候才会返回true。
CAS思想在实际生产应用中很常见,比如多线程更新发票额度的使用情况,除了使用分布式锁也可以使用CAS思想,对发票额度加上一个版本号,第一个线程要做修改时先读取发票的额度和版本,计算完之后更新时在where条件中指定版本号等于刚才读到的版本号,并将版本号+1。如果版本号已经被别的线程修改过了,不一致就会更新失败,失败后可以设置重新读取记录并计算重试几次。
3、synchronized
3.1、原理
JAVA中的线程是映射到操作系统原生线程之上的,如果要阻塞或者唤醒线程需要操作系统帮忙。从用户态切换到内核态需要消耗很多资源。
private Object lock = new Object();
private int value = 0;
public void setValue() {
synchronized (lock) {
value++;
}
}
如上代码中,alue++ 因为被关键字synchronized修饰,所以会在各个线程间同步执行,但是value++消耗的时间很有可能比线程状态转换消耗的时间还短。所以说synchronized是java语言中一个重量级的操作
synchronized的核心内容是对象头和监视器:
JAVA对象在内存中的内存模型分为三部分:对象头,实例数据、对齐数据。在JAVA中new一个对象时,就会在内存中创建一个instanceOopDesc对象,该对象中包含了对象头和实例数据
instanceOopDesc的基类为oopDesc类,结构如下:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
- 其中 __mark 和 metadata 一起组成对象头。
- _metadata主要保存了类元数据。
- _mark 是 markOop类型数据,一般称为标记字段(Mark Word),其中主要存储了对象的hashCode,分代年龄,锁标志位,是否偏向锁等。
32位Java虚拟机的Mark Word的默认存储结构如下:
默认情况下,没有线程进行加锁操作,所以对象中的Mark Word处于无锁状态。
- 考虑到jvm的空间效率,Mark Word被设计成一个非固定的数据结构,以便存储更多的有效数据。
- 他会根据对象本身的状态复用自己的存储空间,如32位jvm下,除了Mark Word的默认存储结构外,还有如下可能存在的结构
- 从图中可以看出,根据“锁标志位”以及“是否为偏向锁”,Java中的锁可以分为以下几种状态:
是否偏向锁 | 锁标志位 | 锁状态 |
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
- 当锁是重量级锁时,对象头中Mark Word会用30bit来指向一个“互斥量”,这个互斥量就是Monitor。
Monitor
- Monitor可以理解为一个同步工具,也可以描述为一种同步机制。它是一个保存在对象头_mark中的一个对象。
- 在markOop类中有一个方法monitor() 会创建一个ObjectMonitor对象,OjbectMonitor就是Java虚拟机中的Monitor的具体实现。
- 因此Java中每个对象都会有一个对应的ObjectMonitor对象,这也是Java中所有的Object都可以作为锁对象的原因。
ObjectMonitor实现锁同步机制
ObjectMonitor对象结构:
ObjectMonitor(){
_header = NULL;
_count = 0; //记录个数
_waiters = 0;
_recursions = 0; //锁重入次数
_object = NULL;
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock= 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_EntryList = NULL; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread =0;
}
解析
- 当多个线程同时访问一段同步代码时,线程首先会进入_EntryList队列中,当某个线程通过竞争获取到对象的monitor后,monitor会把 _owner变量设置为当前线程,同时monotor中的计数器 _count加1,即获得对象锁
- 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null, _count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。
- 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
3.2、synchronize锁状态
- 偏向锁
- 偏向锁在只有一个线程并发时才会触发到,不存在多线程竞争的情况,那么会判断在Monitor的_owner线程是当前线程,一致直接进入。
- 轻量级锁
- 当线程竞争比较激烈的时候,偏向锁就会升级成轻量级锁,通过用户态自旋的方式等待上一个线程释放锁。
- 重量级锁
- 当并发加剧,线程的自旋超过一定的次数,轻量级锁就会升级成重量级锁,重量级锁会使所有的未获取锁的线程都阻塞等待,阻塞状态消耗CPU较少。
- 锁粗化
- 本质上是对多次加锁加锁解锁的代码合并成一块
- 锁消除
- 如果加锁的代码块或方法没有线程竞争或者多余加锁,JVM会将加锁逻辑进行优化。
3.3、synchronized应用
在JAVA中被synchronized修饰的代码块或函数都是并发安全的。
synchronized关键字一般被用在以下地方:
3.3.1、synchronized修饰实例方法。
package thread;
import java.util.Objects;
public class TestSynchronizedObject {
public int count = 100;
public Object lock = new Object();
public synchronized void SellerMarket() {
do {
System.out.println("受理请求");
if (count == 0) {
System.out.println("我的名字是:" + Thread.currentThread().getId() + " 今日票已卖完,请明日再来");
return;
} else {
System.out.println("我的名字是:" + Thread.currentThread().getId() + " 卖出去一张票,票号是:" + count);
count--;
}
} while (true);
}
public static void main(String[] args) throws InterruptedException {
TestSynchronizedObject testSynchronizedObject = new TestSynchronizedObject();
Thread thread1 = new Thread(() -> testSynchronizedObject.SellerMarket());
Thread thread2 = new Thread(() -> testSynchronizedObject.SellerMarket());
Thread thread3 = new Thread(() -> testSynchronizedObject.SellerMarket());
Thread thread4 = new Thread(() -> testSynchronizedObject.SellerMarket());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
}
3.3.2、synchronized修饰静态方法。
- 保障线程安全性,避免了多个线程同时访问和修改共享的静态数据的问题。
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
- 类级别的同步:静态方法属于类而不是实例,所以不论是通过哪个实例调用静态方法,都会共享相同的类锁。这可以保证不同实例间的静态方法的操作是同步的
public class Printer {
public static synchronized void print(String message) {
System.out.println(message);
}
}
- 全局访问控制:通过
synchronized
修饰静态方法可以控制对该类的静态方法的访问。当线程需要执行该静态方法时,必须先获取类锁,其他线程将被阻塞,从而实现对全局资源的访问控制。
public class Database {
public static synchronized void writeData(String data) {
// 写入数据到数据库
}
}
3.3.3、synchronized修饰实例方法的代码块。
package thread;
import java.util.Objects;
public class TestSynchronizedObject {
public int count = 100;
public void SellerMarkeThis() {
do {
System.out.println("受理请求");
synchronized (this) {
if (count == 0) {
System.out.println("我的名字是:" + Thread.currentThread().getId() + " 今日票已卖完,请明日再来");
return;
} else {
System.out.println("我的名字是:" + Thread.currentThread().getId() + " 卖出去一张票,票号是:" + count);
count--;
}
}
} while (true);
}
public static void main(String[] args) throws InterruptedException {
TestSynchronizedObject testSynchronizedObject = new TestSynchronizedObject();
Thread thread1 = new Thread(() -> testSynchronizedObject.SellerMarkeThis());
Thread thread2 = new Thread(() -> testSynchronizedObject.SellerMarkeThis());
Thread thread3 = new Thread(() -> testSynchronizedObject.SellerMarkeThis());
Thread thread4 = new Thread(() -> testSynchronizedObject.SellerMarkeThis());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
}
3.3.4、synchronized修饰静态方法的代码块。
public class Printer {
public static void print(String message) {
synchronized (Printer.class) {
System.out.println(message);
}
}
}
修饰代码块而不直接修饰方法是为了提高灵活性和更细粒度并发控制。
3.3.5、应该用什么对象作为锁对象
事实上任何对象都可以作为锁对象,但是不推荐使用字符串和基本类型的包装类作为锁对象。因为字符串对象和基本类型的包装类会有缓存的问题,字符串有字符串常量池,整数有小整数池,在使用这些对象的时候他们最后都有可能指向同一个对象,获取锁的难度就会增加,降低程序的并发性。
3.3.6、synchronized的可见性和重排序
可见性:当线程进入到synchronized代码的时候,将会刷新所有对该线程可见的变量,当一个线程从同步代码块退出的时候也会将线程的工作内存同步到内存当中,保证在同步代码块当中修改的变量对其他线程可见。
重排序:指令重排序是编译器和处理器为了优化程序性能而进行的一种重排指令执行顺序的优化技术。在多线程环境下,指令重排序可能导致线程间操作的顺序与程序源代码的顺序不一致,从而产生错误的结果或破坏线程安全性。对于synchronized
关键字,它保证了以下两个原则:
1、进入同步块前的指令不能排在同步块中的指令之后执行(禁止重排序到后方)。
2、在同步块中的指令不能排在同步块后的指令之前执行(禁止重排序到前方)。
这样,synchronized
关键字有效地避免了线程安全问题,确保了正确的执行顺序和可见性。
4、ReentrantLock
ReentrantLock是一个用户态的CAS锁
4.1、常用方法
方法 | 说明 |
lock() | 尝试加锁,死等 |
tryLock() | 尝试加锁,有超时时间 |
unlock() | 解锁 |
Condition | 条件变量 |
4.2、使用
public class test{
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
//尝试加锁,死等
lock.tryLock();
//尝试加锁,有超时时间
lock.tryLock(1, TimeUnit.SECONDS);
//释放锁
lock.unlock();
}
}
异常处理
public static void exception() {
ReentrantLock lock = new ReentrantLock();
try {
//加锁
lock.lock();
//需要实现的业务
throw new Exception("出现异常");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
ReentrantLock的公平锁和非公平锁
//公平锁 ReentrantLock lock = new ReentrantLock(true);//非公平锁
ReentrantLock lock = new ReentrantLock(false);
ReentrantLock的读写锁
public class TestReadWriteLock {
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//读锁,共享锁,读与读可以共享
lock.writeLock();
//写锁,排它锁,读写,读读,写读不能共存
lock.readLock();
}
}
Condition条件变量
package thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock {
/**
* ReentrantLock可以根据不同的Condition去休眠或唤醒线程
* 同一把锁可以分为不同的休眠或唤醒条件
*/
private static ReentrantLock reentrantLock = new ReentrantLock();
// 定义不同的条件
private static Condition boyCondition = reentrantLock.newCondition();
private static Condition girlCondition = reentrantLock.newCondition();
ReentrantLock lock = new ReentrantLock();
public static void main (String[] argc) throws InterruptedException {
Thread threadBoy = new Thread(() -> {
// 让处理男生任务的线程去休眠
try {
boyCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒处理女生任务的线程
girlCondition.signalAll();
});
Thread threadGirl = new Thread(() -> {
// 让处理女生任务的线程去休眠
try {
girlCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒处理男生任务的线程
boyCondition.signalAll();
});
}
}
4.3、ReentrantLock和synchronized的区别
1、synchronized 使用时不需要手动释放锁.ReentrantLock使用时需要手动释放.使用起来更灵活,但是也容易遗漏unlock.
2、synchronized在申请锁失败时,会一直等待锁资源.ReentrantLock可以通过trylock的方式等待一段时间就放弃.
3、synchronized是非公平锁, ReentrantLock 默认是非公平锁.可以通过构造方法传入一个true开启公平锁模式.
4、ynchronized是一个关键字,是JVM内部实现的(可能涉及到内核态).ReentrantLock是标准库的一个类,基于Java JUC实现(用户态实现)
5、CountDownLatch-闭锁
同时等待 N 个任务执行结束,比如10个人赛跑,需要等待10个人都跑完之后,才能公布所有人的成绩。
package thread;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class TestCountDownLatch {
private static CountDownLatch countDownLatch = new CountDownLatch(11);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(
()-> {
System.out.println("出发,我的名字是:"+Thread.currentThread().getName());
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行完毕
countDownLatch.countDown();
});
thread.start();
thread.join();
}
//等待所有线程执行完毕,再执行下面的内容
countDownLatch.await();
System.out.println("公布成绩");
}
}
6、CyclicBarrier-循环栅栏
当我们需要多个线程同时达到一个临界点时,可以使用CyclicBarrier
。CyclicBarrier
类允许我们指定在多个线程都到达屏障前需要等待的数量,并在达到该数量时执行一个动作。
package thread;
import java.util.concurrent.*;
public class TestCyclicBarrier {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
达到特定数量之后,再执行一个操作,将要执行的操作可以写在初始化CyclicBarrier时传入
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int numberOfThreads = 3;
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
System.out.println("All threads have reached the barrier.");
// 在所有线程到达屏障后执行的动作
});
for (int i = 0; i < numberOfThreads; i++) {
Thread thread = new Thread(new Worker(barrier));
thread.start();
}
}
static class Worker implements Runnable {
private final CyclicBarrier barrier;
Worker(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println("Thread " + Thread.currentThread().getId() + " is performing some work.");
Thread.sleep(2000); // 模拟一些工作
System.out.println("Thread " + Thread.currentThread().getId() + " has reached the barrier.");
barrier.await(); // 等待其他线程到达屏障
System.out.println("Thread " + Thread.currentThread().getId() + " continues execution after the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}