其实很简单,大家都喜欢缩写!J.U.C= java.util.concurrent就是这个东西
在Lock接口出现之前,java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合所有的并发场景。但是在java5以后,Lock的出现可以解决synchronized在某些场景中的短板,它比synchronized更加灵活
看下面的案例: ReentrantLock的Demo
public class ReentrantLockTest1 {
static int value = 0;
Lock lock = new ReentrantLock();
public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
value++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
incr();
});
}
// 启动线程
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
System.out.println("value的值为:" + value);
}
}
结果: value的值为:960
很明显这个结果不是我们想要的!我们想要的是: 1000
继续往下看:
public class ReentrantLockTest1 {
static int value = 0;
static Lock lock = new ReentrantLock();
public static void incr() {
try {
lock.lock();
value ++;
try {
Thread.sleep(1);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
incr();
});
}
// 启动线程
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
System.out.println("value的值为:" + value);
}
}
结果: value的值为:89
说明什么?完整获取锁的执行只有89次,我们在改变一下
接着看下面的案例:
public class ReentrantLockTest1 {
static int value = 0;
static Lock lock = new ReentrantLock();
public static void incr() {
try {
lock.lock();
value++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
incr();
});
}
// 启动线程
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
Thread.sleep(3000);
System.out.println("value的值为:" + value);
}
}
结果: value的值为:1000
以上得出的结论是: ReentrantLock.lock() 确实可以保证多线程情况下的线程安全,前提是你得让他执行完!
在上面执行的工程中我们发现一个问题我们尝试过用ReentrantLock.tryLock() 去尝试获得锁,但是存在一个问题:
public class ReentrantLockTest1 {
static int value = 0;
static Lock lock = new ReentrantLock();
public static void incr() {
if (lock.tryLock()) {
value++;
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
incr();
});
}
// 启动线程
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
Thread.sleep(10000);
System.out.println("value的值为:" + value);
}
}
前提: 我试过把睡眠时间调整为 3、7、7、10秒,但是得到的结果都是不足1000
这样子说来
ReentrantLock.lock()
ReentrantLock.tryLock()
存在很大区别了
从结果上看:ReentrantLock.lock()最起码能保证结果的正确性
ReentrantLock.tryLock()不能保证结果的正确性
我们先去看下ReentrantLock.tryLock()因为Lock()的底层原理我已经比较熟悉了
代码如下:
/**
* 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();
int c = getState();
if (c == 0) {
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;
}
ReentrantLock.lock()最起码能保证结果的正确性的原因是:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
它将为获取到锁的线程放置到了一个等待队列(双向链表)中
所以lock() tryLock() 从本质上讲还是存在很大区别的!!!
下面我们再说下: ReentrantReadWriteLock(重入读写锁)
看下面的案例:
public class Demo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock();
static Lock write = rwl.writeLock();
static Lock fromLock = new ReentrantLock();
public static Object get(String key) {
if (fromLock.tryLock())
// 读锁 阻塞
read.lock();
try {
return cacheMap.get(key);
} finally {
read.unlock();
}
}
public static Object write(String key, Object value) {
// other thread 获得了写锁
write.lock();
try {
return cacheMap.put(key, value);
} finally {
write.unlock();
}
}
}
说明: 当多个线程访问get()/write()方法的时候,当多个线程读一个变量的时候是不互斥的,但是当一个线程获取了写锁,那么此时
读锁会阻塞,防止拿到当数据
Ps: ReentrantReadWriteLock适用于读多写少的场景
但是!究竟尼玛为啥,当获取写锁的时候读锁会阻塞?我们去看看
/**
* A {@code ReadWriteLock} maintains a pair of associated {@link
* Lock locks}, one for read-only operations and one for writing.
* The {@link #readLock read lock} may be held simultaneously by
* multiple reader threads, so long as there are no writers. The
* {@link #writeLock write lock} is exclusive.
*/
我感觉已经说的很明显了。。实际上是因为位置,没有看到具体的实现
上面的问题呢?先放着吧,暂时超出我的能力,需要指引!!!
思考锁的实现(设计思维)
1、锁的互斥
2、没有抢占到锁的线程?
3、等待的线程怎么存储?
4、公平和非公平(能否插队)
5、重入的特性(识别是否同一个人?ThreadID)
解决方案:
1、锁的互斥,说的在简单点就是就共享资源的竞争,巧的是以前抢夺的是共享资源!现在抢占的是一个标志位!state,如果state=0那么代表当前线程没有抢占到锁,如果state=1则代表抢占到了锁,可以继续向下执行
2、3 没有抢占到锁的线程我们该如何处理?等待的线程怎么存储?我们可以举例下面的一个场景,好比去医院看病,这个例子不好!换一个~假如我们去洗脚城洗脚吧,我们中意7号!但是奈何喜欢她的人比较多,老板只能让你等着等7号空闲出来了,你才能上!用词错误,你才能洗~ 但是,不可能说我先来的我最后一个上是吧,所以老板需要给我发一个号码牌,假定是9527号,按照正常来讲一定是顺序排队的,谁先来,谁上!
4、这个公平不公平我们沿用上面的例子!正常来说一定是谁先来的谁先上,但是存在一个问题,一个新来的大哥,看队伍比较长,他想先洗,不洗就挂了!拿500块买我的位置~ 我可能也不会卖,除非给我550!如果我卖他了,那就是不公平的(大哥插队了),如果我大喝一声: 这世道竟然还有插队的!?他可能就得老老实实排队去了,那么就是公平的,因为得排队
5、重入性这个就比较有意思了~ 7号给大爷,再加个钟!!,懂的都懂。。不能再说了
技术方案:
1、volatile state = 0;(无锁), 1代表是持有锁, > 1代表重入
2、wait/notify马上到!condition 需要唤醒指定线程。【LockSupport.park(); -> unpark(thread)】 unsafe类中提供的一个方法
3、双向链表
4、逻辑层面实现
5、在某一个地方存储当前获得锁的线程的ID,判断下次抢占锁的线程是否为同一个
下面我们来模拟一个场景: 模拟三个线程争夺lock()的场景(先把总体的图给你们,再去看源码分析)
/**
* Acquires the lock.
*
* If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
*
*
Implementation Considerations
*
*
A {@code Lock} implementation may be able to detect erroneous use
* of the lock, such as an invocation that would cause deadlock, and
* may throw an (unchecked) exception in such circumstances. The
* circumstances and the exception type must be documented by that
* {@code Lock} implementation.
*/
void lock();
说的什么意思呢?
1、尝试获取锁
2、在获取锁的过程中如果发现当前锁没抢到那么,当前线程会变为阻塞状态进入休眠状态
3、当持有锁的线程释放掉锁,那么休眠的线程就可以去竞争锁
/**
* Acquires the lock.
*
* Acquires the lock if it is not held by another thread and returns
* immediately, setting the lock hold count to one.
*
*
If the current thread already holds the lock then the hold
* count is incremented by one and the method returns immediately.
*
*
If the lock is held by another thread then the
* current thread becomes disabled for thread scheduling
* purposes and lies dormant until the lock has been acquired,
* at which time the lock hold count is set to one.
*/
public void lock() {
sync.lock();
}
这个是ReentrantLock里面的lock()说的什么意思呢?
1、如果当前锁没有被持有那么当前线程持有锁,并且将持有次数设置为1
2、如果当前线程已经持有了锁,那么持有次数 + 1,并且立即返回表示持有锁
3、同上
这个sync是啥?瞅一瞅
提供所有实现机制的同步器,基于AQS去表示当前锁的状态,成吧(我是没理解)
说下我的理解吧
保证锁状态"state"的实时性,这东西就是干这个的!
我们接着看非公平锁的实现
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
分析: (我们多分析点,多个线程抢夺锁的情况,分析如图的情况吧ThreadA、ThreadB、ThreadC)
第一次,刚刚进入,此时state = 0, 那么我们进入if分支
setExclusiveOwnerThread(Thread.currentThread());
注释如下:
/**
* Sets the thread that currently owns exclusive access.
* A {@code null} argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* {@code volatile} field accesses.
* @param thread the owner thread
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
解释:
它说了一堆没有的没用的,总结就是一句话: 表示当前这个线程拥有了锁,可以去访问了!没了。
总结: 第一次进入做了什么事呢?
1、设置state 0 ---> 1
2、设置exclusiveOwnerThread 为当前线程
(我画的图还是蛮好的!!!)
那么当一个线程持有锁,其他线程进入是什么样子的一个情况呢?我们继续分析
它会进入else分支,那么如下:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
注解解释:
忽略打断,至少调用一次tryAcquire(),尝试去获取锁
换句话说,线程在一个队列中可能被再次阻塞和释放,不断调用tryAcquire()
方法直到成功,该方法被调用一般在实现了Lock接口(听不出什么东西),不过可以知晓下面两点:
1、阻塞的线程在队列中
2、阻塞的线程会调用tryAcquire()方法
我们再来仔细分析下acquire(int arg),这里面调用了什么方法,呵~好家伙,可不少
1、tryAcquire(arg)
2、addWaiter(Node.EXCLUSIVE)
3、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
三个方法,我们一个一个来分析
1、tryAcquire(arg),其实在分析它前我们可以猜一下这个方法干了什么?
A、查看当前的state是否变为了0,如果为零了,那么就返回
养成好习惯,看源码前要先读注释,要先在总体上有一个把握,再去看具体的实现,不然,你看个什么玩意,听话养成好习惯,别看一大串子,别急,源码急不来的
差距就是在一点一滴中养成的
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*
* This method is always invoked by the thread performing
* acquire. If this method reports failure, the acquire method
* may queue the thread, if it is not already queued, until it is
* signalled by a release from some other thread. This can be used
* to implement method {@link Lock#tryLock()}.
*
*
The default
* implementation throws {@link UnsupportedOperationException}.
*
* @param arg the acquire argument. This value is always the one
* passed to an acquire method, or is the value saved on entry
* to a condition wait. The value is otherwise uninterpreted
* and can represent anything you like.
* @return {@code true} if successful. Upon success, this object has
* been acquired.
* @throws IllegalMonitorStateException if acquiring would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们来一起读,其实我也没看过这里,也是新的知识,这是我的学习方法,我感觉还不错吧
1、尝试去获取独占模式(也就是去获取这个锁)
2、当state 准许被访问的时候,访问这个方法的线程应该是有序的排队访问
3、如果说线程没有获取到state那么它可能会进等待队列中,如果它没有在等待队列中话(这里面是有说法的 a、等待队列中的线程去顺序获取state b、未在队列中的也可以竞争)
4、以上的所有前提是: signalled by a release(state)
Ps: 其实说的已经很明显了!你看我们上面的图,没有获取到锁的线程,它会进入到一个双向的等待队列中
继续往下看:
/**
* 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();
int c = getState();
if (c == 0) {
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,并设置线程为自己
2、如果获取state的线程为当前持有state的线程,那么重入次数 + 1
下面我们来分析第二个方法: addWaiter(Node.EXCLUSIVE), arg)
这个中规中矩,其实还可以吧
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
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;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
解释:
添加节点至双向队列,节点以给定的模式进行存储,如果当前队列存在节点,那么进入if分支,如果不存在节点那么走非if分支,我们接着看这两个分支
我们这个先进入enq(node);这个方法
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
解释: 可以看出以下知识点:
1、尾插法 Node t = tail
2、当队列中不存在元素的时候那么tail = head = new Node
3、else 分支node.prev = t其实执行的操作就是新插入元素的前一个元素为原队列的尾节点,那么可以判断
新插入的元素必定为队列的尾节点
4、我们看下compareAndSetTail(t, node),应该指的就是我们上面的操作,点进去之后发现是一个native方法,但是可以推测和我们猜测差不多的
5、compareAndSetHead(new Node()) 这个方法点进去也是native的至于功能我们也阐述过了
Ps: 再来看下我们的图: 没有获得锁的线程,是不是很神奇
我们接着往下看,第三个方法: 是以第二个方法返回的Node作为参数
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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 (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解释下:
通俗点解释就是,将这个未获取到锁的Node丢到等待队列中,当锁可以被竞争了"state"那么他就活了
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
返回当前节点的前一个节点
继续研究: final boolean acquireQueued(final Node node, int arg)
那么如下: 说了啥呢
1、如果当前节点的前一个节点为头结点并且尝试获取锁成功!那么将node设置为当前等待队列的head节点
2、如果不成立的话,说明当前锁还是不可获取的状态这时判断是否可以挂起当前线程、
3、如果判断结果为真则挂起当前线程, 否则继续循环,
4、在这期间线程不响应中断
5、在最后确保如果获取失败就取消获取
if (failed) {
cancelAcquire(node);
}
我目前的水平值准许我分析到这种程度了。。以后找到对象我再继续分析,哈哈! 再见。
有问题,大家一起讨论,不开心你骂我也成,但是你得说出所以然,不然我可能会去打你。。
图感觉有点花,可以看这个: https://app.yinxiang.com/fx/279855bd-bcda-462e-be8f-e69ab987df95