synchronized同步代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞
/**
* @company: 拓薪教育
* @author: 大亮老师 QQ:206229531
*/
public class Test1 {
public static void main(String[] args) {
Task t = new Task();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
/**
线程任务类
*/
static class Task implements Runnable{
@Override
public void run() {
//同步代码块
synchronized (this){
try {
//休眠一秒
TimeUnit.SECONDS.sleep(1);
//执行业务逻辑
System.out.println(Thread.currentThread().getName()+"正在执行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
JUC中重入锁ReentrantLock实现同步锁
/**
* @company: 拓薪教育
* @author: 大亮老师 QQ:206229531
*/
public class Test2 {
/*
定义重入锁
*/
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
Task t = new Task();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
/**
线程任务类
*/
static class Task implements Runnable{
@Override
public void run() {
//☆☆☆加锁☆☆☆
lock.lock();
try {
//休眠1秒
TimeUnit.SECONDS.sleep(1);
//执行业务逻辑
System.out.println(Thread.currentThread().getName()+"正在执行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//☆☆☆解锁☆☆☆
lock.unlock();
}
}
}
}
ReentrantLock和synchronized的不同
- Synchronized无法响应中断
- Synchronized无法得知是否获得到锁
- Synchronized无法控制锁超时处理
- 多线程读操作效率低
- 默认非公平锁,无法实现公平锁
我们可以看下面的例子,我们发现Synchronized是不能被中断的。
/**
* @company: 拓薪教育
* @author: 大亮老师 QQ:206229531
*/
public class Test3 {
//创建两个对象(锁)
static Object lock1 = new Object();
static Object lock2 = new Object();
static int flag = 1;
/**
* 定义线程实现类
*/
static class TxTask implements Runnable {
//标识属性
int flag;
//构造器
public TxTask(int flag) {
this.flag = flag;
}
@Override
public void run() {
//为了产生死锁通过flag来让两个线程进入不同分支
if (flag == 1) {
//持有锁1
synchronized (lock1) {
System.out.println("进入锁1");
//获得锁2
synchronized (lock2) {
System.out.println("进入锁1中的锁2");
}
}
} else {
//持有锁2
synchronized (lock2) {
System.out.println("进入锁2");
//获得锁1
synchronized (lock1) {
System.out.println("进入锁2中的锁1");
}
}
}
}
}
public static void main(String[] args) {
TxTask task = new TxTask(1);
TxTask task1 = new TxTask(0);
Thread t = new Thread(task);
Thread t1 = new Thread(task1);
t.start();
t1.start();
t.interrupt();
}
}
ReentrantLock可以中断
public class Test4 {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
static class Task implements Runnable {
Lock lock1;
Lock lock2;
public Task(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"执行逻辑...");
TimeUnit.MILLISECONDS.sleep(100);
lock2.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
}
}
}
public static void main(String[] args) {
Task t = new Task(lock1, lock2);
Task t1 = new Task(lock2, lock1);
Thread thread = new Thread(t);
Thread thread1 = new Thread(t1);
thread.start();
thread1.start();
thread.interrupt();
}
}
ReentrantLock可以尝试获取锁和控制获取锁的超时时间
public class Test5 {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
static class Task implements Runnable {
Lock lock1;
Lock lock2;
public Task(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
//自旋
while (!lock1.tryLock()) {
TimeUnit.MILLISECONDS.sleep(10);
}
System.out.println(Thread.currentThread().getName() + "执行逻辑...");
TimeUnit.MILLISECONDS.sleep(10);
while (!lock2.tryLock()) {
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock2.unlock();
}
}
}
public static void main(String[] args) {
Task t = new Task(lock1, lock2);
Task t1 = new Task(lock2, lock1);
Thread thread = new Thread(t);
Thread thread1 = new Thread(t1);
thread.start();
thread1.start();
}
}
手写实现排他锁:方式1
- 实现流程
- 代码实现
public class Test6 {
static TxLock lock = new TxLock();
public static void main(String[] args) {
Task t = new Task();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
static class Task implements Runnable{
@Override
public void run() {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"正在执行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class TxLock{
/**
* 0 表示没有获得锁
* 1 拥有锁
* Unsafe:直接操作内存的类,比较危险,不推荐大家直接操作内存。
*/
static final Unsafe unsafe;
//内存上的偏移量
static final long stateOffset;
//要在内存上操作的可见变量
volatile long status = 0;
static {
try {
//使用反射获取Unsafe的成员变量theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//设置为可存取
field.setAccessible(true);
//获取该变量的值
unsafe = (Unsafe) field.get(null);
//获取state在TestUnSafe中的汇编语言偏移量
stateOffset = unsafe.objectFieldOffset(TxLock.class.getDeclaredField("status"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
/**
* CAS
*
*/
public void lock(){
//想要把status改成1
while (!unsafe.compareAndSwapInt(status, stateOffset, 0, 1)){
}
}
public void unlock(){
//想要把status改成0
while (!unsafe.compareAndSwapInt(status, stateOffset, 1, 0)){
}
}
}
}
手写实现锁:方式2
- 实现流程
- 代码实现
public class Test7 {
static TxLock lock = new TxLock();
public static void main(String[] args) {
Task t = new Task();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
static class Task implements Runnable{
@Override
public void run() {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"正在执行...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class TxLock{
ConcurrentLinkedQueue<Thread> threads = new ConcurrentLinkedQueue<>();
/**
* 0 表示没有获得锁
* 1 拥有锁
*
*/
static final Unsafe unsafe;
static final long stateOffset;
volatile long status = 0;
static {
try {
//使用反射获取Unsafe的成员变量theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//设置为可存取
field.setAccessible(true);
//获取该变量的值
unsafe = (Unsafe) field.get(null);
//获取state在TestUnSafe中的汇编语言偏移量
stateOffset = unsafe.objectFieldOffset(TxLock.class.getDeclaredField("status"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
/**
* CAS
*
*/
public void lock(){
//想要把status改成1
while (!unsafe.compareAndSwapInt(status, stateOffset, 0, 1)){
threads.offer(Thread.currentThread());
LockSupport.park();
}
}
public void unlock(){
//想要把status改成0
while (unsafe.compareAndSwapInt(status, stateOffset, 1, 0)){
Thread thread = threads.poll();
LockSupport.unpark(thread);
}
}
}
}
ReentrantLock源码分析
公平锁内部是FairSync,非公平锁内部是NonfairSync。而不管是FairSync还是NonfariSync,都间接继承自AbstractQueuedSynchronizer这个抽象类
- ReentrantLock分为公平锁和非公平锁,源码类图
该抽象类为我们的加锁和解锁过程提供了统一的模板方法,只是一些细节的处理由该抽象类的实现类自己决定。所以在解读ReentrantLock(重入锁)的源码之前,有必要了解下AbstractQueuedSynchronizer。
AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。
AQS类结构Node节点代码片段
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//线程被取消
static final int CANCELLED = 1;
//线程处于正常需要被唤醒的状态
static final int SIGNAL = -1;
//条件挂起状态
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
/**
* 当前节点的前指针
*/
volatile Node prev;
/**
* * 当前节点的尾指针
*/
volatile Node next;
/**
* 当前节点持有的线程
*/
volatile Thread thread;
Node nextWaiter;
AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时可以更直观形象,况且引用本身是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。
head是头指针,指向队列的首元素;tail是尾指针,指向队列的尾元素。而队列的元素结点Node定义在AQS内部,主要有如下几个成员变量
- prev:指向前一个结点的指针
- next:指向后一个结点的指针
- thread:当前结点表示的线程,因为同步队列中的结点内部封装了之前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用
- waitStatus:对于重入锁而言,主要有3个值。0:初始化状态;-1(SIGNAL):当前结点表示的线程在释放锁后需要唤醒后续节点的线程;1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待。
ReentrantLock加锁的逻辑代码
/**
* 加锁逻辑
*/
final void lock() {
//通过cas方式把state从0更新成1
if (compareAndSetState(0, 1))
//获取锁成功则将当前线程标记为持有锁的线程,然后直接返回
setExclusiveOwnerThread(Thread.currentThread());
else
//如果获得锁失败的后续操作
acquire(1);
}
首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行acquire方法
获取锁逻辑
当我们第一次获取锁失败后,我们会尝试再获取一次调用acquire(int arg)
/**
* tryAcquire(arg) 是AQS中定义的钩子方法:
* 该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现
* addWaiter() 获取锁失败的线程如何安全的加入同步队列
*
* acquireQueued() 新节点线程加入同步队列后挂起
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法tryAcquire(),addWaiter()和acquireQueued(),这三个方法中分别封装了加锁流程中的主要处理逻辑,理解了这三个方法到底做了哪些事情,整个加锁流程就清晰了。
非公平锁获取锁
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
* 非公平锁获取锁的逻辑
*/
final boolean nonfairTryAcquire(int acquires) {
//获得当前正在运行的线程
final Thread current = Thread.currentThread();
//获取state变量的值,即当前锁被重入的次数
int c = getState();
//state为0,说明当前锁未被任何线程持有
if (c == 0) {
//使用cas方式获取锁
if (compareAndSetState(0, acquires)) {
//如果获得成功,把当前线程设置为持有线程
setExclusiveOwnerThread(current);
//得到锁返回结果
return true;
}
}//如果当前线程就是持有线程
else if (current == getExclusiveOwnerThread()) {
//把重入次数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置重入次数
setState(nextc);
//得到锁返回结果
return true;
}
//没有获得到锁
return false;
}
这是非公平模式下获取锁的通用方法。它囊括了当前线程在尝试获取锁时的所有可能情况:
1.当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,然后返回成功的结果;若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。
2.若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。
3.若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果
因为我们用state来统计锁被线程重入的次数,所以当前线程尝试获取锁的操作是否成功可以简化为:state值是否成功累加1,是则尝试获取锁成功,否则尝试获取锁失败。
创建node节点逻辑
tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看addWaiter(Node.EXCLUSIVE)
部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。这部分代码可以说是整个加锁流程源码的精华,充分体现了并发编程的艺术性。
private Node addWaiter(Node mode) {
//首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//尝试快速入队
//定义尾部临时节点
Node pred = tail;
//如果尾部不是空
if (pred != null) {
//把新节点的前指针设置成尾节点
node.prev = pred;
//CAS方式把新节点设置尾节点
if (compareAndSetTail(pred, node)) {
//把老的尾节点的尾指针设置成新节点
pred.next = node;
//返回新阶段
return node;
}
}
//自旋入队
enq(node);
return node;
}
首先创建了一个新节点,并将当前线程实例封装在其内部,之后我们直接看enq(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分“重复代码”和尝试获取锁时的“重复代码”一样,对某些特殊情况
进行提前处理,牺牲一定的代码可读性换取性能提升。
- 创建node节点自旋入队
private Node enq(final Node node) {
//
for (;;) {
//定义尾部临时节点
Node t = tail;
//首节点情况
if (t == null) { // Must initialize
//当前阶段CAS方式作为头阶段
if (compareAndSetHead(new Node()))
//同时尾阶段也设置成新节点
tail = head;
} else {
//新节点的前指针设置成旧的尾节点
node.prev = t;
//CAS方式设置尾节点
if (compareAndSetTail(t, node)) {
//旧的尾节点指向新节点
t.next = node;
return t;
}
}
}
}
这里有两个CAS操作:
compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功
//当前阶段CAS方式作为头阶段
if (compareAndSetHead(new Node()))
//同时尾阶段也设置成新节点
tail = head;
外层的for循环保证了所有获取锁失败的线程经过失败重试后最后都能加入同步队列。因为AQS的同步队列是不带哨兵结点的,故当队列为空时要进行特殊处理,这部分在if分句中。注意当前线程所在的结点不能直接插入
空队列,因为阻塞的线程是由前驱结点进行唤醒的。故先要插入一个结点作为队列首元素,当锁释放时由它来唤醒后面被阻塞的线程,从逻辑上这个队列首元素也可以表示当前正获取锁的线程,虽然并不一定真实持有其线程实例。
首先通过new Node()创建一个空结点,然后以CAS方式让头指针指向该结点(该结点并非当前线程所在的结点),若该操作成功,则将尾指针也指向该结点。
compareAndSetTail(t, node),CAS方式更新tial指针,仅当原值为t时更新成功
if (compareAndSetTail(t, node)) {
//旧的尾节点指向新节点
t.next = node;
return t;
}
首先当前线程所在的结点的前向指针pre指向当前线程认为的尾结点,源码中用t表示。然后以CAS的方式将尾指针指向当前结点,该操作仅当tail=t,即尾指针在进行CAS前未改变时成功。若CAS执行成功,则将原尾结点的后向指针next指向新的尾结点。
整个入队的过程并不复杂,是典型的CAS加失败重试的乐观锁策略。其中只有更新头指针和更新尾指针这两步进行了CAS同步,可以预见高并发场景下性能是非常好的。
节点挂起和获得执行逻辑
final boolean acquireQueued(final Node node, int arg) {
//成功标志
boolean failed = true;
try {
//打断标志
boolean interrupted = false;
for (;;) {
//获得当前新节点的前节点
final Node p = node.predecessor();
//前节点是头, 并且尝试获取锁成功
if (p == head && tryAcquire(arg)) {
//把自己设置成头结点
setHead(node);
//旧的头结点放手
p.next = null; // help GC
//失败为假,即成功
failed = false;
//返回中断标志
return interrupted;
}
//如果上一个if没有发生,即没有获得锁要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果失败
if (failed)
//取消获得锁,资源回收
cancelAcquire(node);
}
}
这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成。第一个if分句中,当前线程首先会判断前驱结点是否是头结点,如果是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。为什么必须前驱结点为头结点才尝试去获取锁?因为头结点表示当前正占有锁的线程,正常情况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是我们希望看到的。然而还有一种情况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不应该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,然后将自身连接在它后面。一旦我们成功获取了锁并成功将自身设置为头结点,就会跳出for循环。否则就会执行第二个if分句:确保前驱结点的状态为SIGNAL,然后阻塞当前线程。
先来看shouldParkAfterFailedAcquire(p, node)判断是否要阻塞当前线程的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获得前节点的等待状态
int ws = pred.waitStatus;
//如果前节点是正常等待唤醒状态返回true
if (ws == Node.SIGNAL)
return true;
//前一个线程被取消等待或者超时
if (ws > 0) {
do {
//向前追溯前面一个正常等待的节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//正常前节点尾指针指向当前新节点
pred.next = node;
} else {
//初识设置
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
可以看到针对前驱结点pred的状态会进行不同的处理
1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程。
2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。
3.pred的状态为初始化状态,此时通过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改为SIGNAL。
其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。
但是要注意只有在前驱结点已经是SIGNAL状态后才会执行后面的方法立即阻塞,对应上面的第一种情况。其他两种情况则因为返回false而重新执行一遍
for循环。
shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程,源码如下:
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程
LockSupport.park(this);
//返回中断标志
return Thread.interrupted();
}
更详细的Spring源码解析请关注:java架构师免费课程
每晚20:00直播分享高级java架构技术
扫描加入QQ交流群264572737