【JUC并发编程系列】深入理解Java并发机制:从synchronized到CAS锁升级全过程(三、synchronized 前置知识)
文章目录
- 【JUC并发编程系列】深入理解Java并发机制:从synchronized到CAS锁升级全过程(三、synchronized 前置知识)
- 1. 基本概念
- 2. 重入锁
- 3. 演示重入锁
- 4. 改造重入锁代码
- 5. 轻量级改造重量级锁
- 6. 公平锁与非公平锁
- 7. 偏向锁/轻量级锁/重量级锁应用场景
【JUC并发编程系列】深入理解Java并发机制:从synchronized到CAS锁升级全过程(三、synchronized 前置知识)
1. 基本概念
- 偏向锁→轻量锁(cas自旋)→重量级锁
- 重入锁概念/悲观锁与乐观锁
- 基于CAS手写-重入锁
- 基于CAS手写类似synchronized锁的升级过程
补充概念:
-
偏向锁: 同一个线程在没有其他线程竞争锁的情况下,可以一直复用我们的锁,不会重复获取锁。
-
轻量锁: 多个线程同时获取锁,只有一个线程获取锁,没有获取到锁的线程会通过CAS不断重试(可以配置重试次数)
-
重量级锁: 重试多次如果没有获取到锁,则当前线程会变成阻塞----用户态切换到内核态/上下文切换
CAS :
-
优点:不需要用户态切换内核态,一直会在我们用户空间中自旋。
-
缺点:有可能会非常的消耗到cpu资源(因为要做自旋)。
如何处理解决呢?
- 控制循环次数
2. 重入锁
重入锁:当前线程如果已经获取到锁,则不会重复的获取锁,而是直接复用。
比如:我们的synchronized/lock
锁如果不具有重入性: 当前线程 递归调用方法中 有可能会发生死锁的问题。
3. 演示重入锁
使用 synchronized
锁
public class Test01 {
public static void main(String[] args) {
a();
}
public static synchronized void a(){
System.out.println("a");
b();
}
public static synchronized void b(){
System.out.println("b");
c();
}
public static synchronized void c(){
System.out.println("c");
}
}
输出 abc
使用自己通过CAS实现的锁 不具备可重入性
public class Test02 {
private static TestLock testLock = new TestLock();
public static void main(String[] args) {
a();
}
public static synchronized void a() {
testLock.lock();
System.out.println("a");
b();
testLock.unlock();
}
public static synchronized void b() {
testLock.lock();
System.out.println("b");
c();
testLock.unlock();
}
public static synchronized void c() {
testLock.lock();
System.out.println("c");
testLock.unlock();
}
}
输出 a
使用jdk并发包中自带的 ReentrantLock
锁
public class Test03 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
a();
}
public static synchronized void a() {
reentrantLock.lock();
System.out.println("a");
b();
reentrantLock.unlock();
}
public static synchronized void b() {
reentrantLock.lock();
System.out.println("b");
c();
reentrantLock.unlock();
}
public static synchronized void c() {
reentrantLock.lock();
System.out.println("c");
reentrantLock.unlock();
}
}
输出 abc
4. 改造重入锁代码
public class TestLock {
/**
* initialValue = 0 表示没有线程使用
* initialValue = 1 表示有一个线程正在使用
*/
private AtomicInteger lockState = new AtomicInteger(0);
/**
* 获取到锁的线程
*/
private Thread ownerLockThread;
/**
* 记录锁的重入次数
*/
private int recursions;
/**
* 获取锁
* 当前线程如果已经获取到锁则直接复用,不需要再次获取
*/
public void lock() {
//当前线程是否已经获取到锁
if (ownerLockThread == Thread.currentThread()) {
//重入次数+1
recursions++;
return;
}
/**
* 将 initialValue 从 0 => 1
* 如果有两个线程同时获取锁,最终只会有一个线程获取成功
* 没有获取到锁会进行自旋
*/
while (true) {
if (lockState.compareAndSet(0, 1)) {
//重入次数+1
recursions++;
//记录获取到锁的线程
ownerLockThread = Thread.currentThread();
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 释放锁
*/
public void unlock() {
if (recursions > 0) {
//重入次数-1
recursions--;
return;
}
//当重入次数为0的时候才会释放锁
while (true) {
if (lockState.compareAndSet(1, 0)) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
5. 轻量级改造重量级锁
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
/**
* @author 赵立
*/
public class TestLock {
/**
* initialValue = 0 表示没有线程使用
* initialValue = 1 表示有一个线程正在使用
*/
private AtomicInteger lockState = new AtomicInteger(0);
/**
* 获取到锁的线程
*/
private Thread ownerLockThread;
/**
* 记录锁的重入次数
*/
private int recursions;
/**
* 重试三次没有获取到锁被阻塞状态的线程
*/
private LinkedBlockingDeque<Thread> notLockThreads = new LinkedBlockingDeque();
/**
* 获取锁
* 当前线程如果已经获取到锁则直接复用,不需要再次获取
*/
public void lock() {
//当前线程是否已经获取到锁
if (ownerLockThread == Thread.currentThread()) {
//重入次数+1
recursions++;
return;
}
/**
* 将 initialValue 从 0 => 1
* 如果有两个线程同时获取锁,最终只会有一个线程获取成功
* 没有获取到锁会进行自旋
*/
/**
* 每个线程获取锁的重试次数
*/
int spinCount = 0;
while (true) {
if (spinCount > 3) {
//重试次数大于三次 让我们当前线程变为阻塞状态
notLockThreads.offer(Thread.currentThread());
LockSupport.park();
//唤醒此线程之后将重试次数改为0次
spinCount = 0;
}
if (lockState.compareAndSet(0, 1)) {
//重入次数+1
recursions++;
//记录获取到锁的线程
ownerLockThread = Thread.currentThread();
return;
}
spinCount++;//重试次数+1
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 释放锁
*/
public void unlock() {
if (recursions > 0) {
//重入次数-1
recursions--;
return;
}
//当重入次数为0的时候才会释放锁
while (true) {
if (lockState.compareAndSet(1, 0)) {
//唤醒所有没有获取到锁的线程,从新开始竞争锁的资源
notifyNotLockThreads();
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 唤醒所有没有获取到锁的线程
*/
public void notifyNotLockThreads() {
notLockThreads.forEach((t) -> LockSupport.unpark(t));
}
}
6. 公平锁与非公平锁
-
公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放,类似于吃饭排队。
-
非公平锁:不是根据根据请求的顺序排列, 通过争抢的方式获取锁。
比如当前 abcd 四个线程,假设A线程获取到锁,在释放锁时:
-
公平锁:根据bcd 请求锁的顺序排列获取锁,B先获取到锁,然后B释放后再给C,以此类推。
-
非公平锁:Bcd 同时竞争这把锁;谁能够抢成功 谁就获取锁成功。
非公平锁效率比较高
Synchronized是非公平锁
//公平锁
new ReentramtLock()(true)
//非公平锁
new ReentramtLock()(false)
公平锁演示
public class Test04 implements Runnable {
private static int count = 0;
// fair 为 true 时为公平锁
private static Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (count < 200) {
createCount();
}
}
public void createCount() {
System.out.print(Thread.currentThread().getName() + ",count:" + count+"; ");
count++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Test04()).start();
}
}
}
非公平锁演示
public class Test04 implements Runnable {
private static int count = 0;
// fair 为 true 时为公平锁
private static Lock lock = new ReentrantLock(false);
@Override
public void run() {
while (count < 200) {
createCount();
}
}
public void createCount() {
System.out.println(Thread.currentThread().getName() + ",count:" + count);
count++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Test04()).start();
}
}
}
上一章节自定义的锁是非公平锁
/**
* 唤醒所有没有获取到锁的线程
*/
public void notifyNotLockThreads() {
/**
* 非公平锁 会唤醒所有的阻塞线程
*/
notLockThread.forEach((t) -> LockSupport.unpark(t));
}
改造为公平锁
/**
* 唤醒所有没有获取到锁的线程
*/
public void notifyNotLockThreads() {
/**
* 公平锁 取出当前队列中的第一个 取出成功时同时删除
*/
LockSupport.unpark(notLockThreads.poll());
}
7. 偏向锁/轻量级锁/重量级锁应用场景
Synchronized 锁的升级过程
-
t1 线程 获取到锁 1毫秒就释放锁 t1线程1毫秒之后又唤醒t2线程竞争锁资源,t2竞争锁资源从阻塞=>就绪=>cpu调度=>竞争锁资源,在这个其中就会有锁的升级过程,由偏向锁 => 轻量级锁
-
如果 t1 线程获取到锁之后 60s 才会释放锁 t2线程自旋60s ,那么 t2 直接阻塞就可以了
-
偏向锁:加锁和解锁不需要额外的开销,只适合于同一个线程访问同步代码块,无需额外的开销,如果多个线程同时竞争的时候,会撤销该锁。
-
轻量级锁:竞争的线程不会阻塞,提高了程序响应速度,如果始终得不到锁的竞争线程,则使用自旋的形式,消耗cpu资源,适合于同步代码块执行非常快的情况下,自旋(jdk1.7以后智能自转)
-
重量级锁: 线程的竞争不会使用自旋,不会消耗cpu资源,适合于同步代码执行比较长的时间。
偏向锁与重入锁之间有什么区别?
-
偏向锁与重入锁实现的思路基本上相同
-
偏向锁(对象头mark word 中会记录线程 id)
-
当前只有一个线程的情况下,没有其他的线程竞争锁,该锁一直会被我们的该线程持有,不会立即释放锁
-
必须要有另外的一个线程竞争该锁,才会撤销我们的偏向锁
-
-
重入锁(会记录该线程的重入次数)
-
当前线程如果已经获取到了锁,当前线程中的其他方法如果有需要获取锁的话,则直接复用
-
当重入次数减到 0 的情况下才会通过
cas
改状态(1,0)
即释放锁
-