多线程方向的锁

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/qq_37933685/article/details/80767809

个人博客:https://suveng.github.io/blog/​​​​​​​

多线程方向的锁

注意:

环境说明:

简单锁

简单的给资源加把锁;以下的所有代码实现都使用同一个类文件.

Counter.java

public class Counter {
    private OrdinaryLock lock = new OrdinaryLock();
    private int count = 0;

    public int inc() throws InterruptedException {
        Thread.sleep(2000);
        lock.lock();//注释掉看区别:TODO
        System.out.println(Thread.currentThread().getId() + "前..." + this.count);
        this.count++;
        System.out.println(Thread.currentThread().getId() + "后..." + this.count);
        lock.unlock();//注释掉看区别:TODO
        return count;
    }
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        final ReentrantClazz reentrantClazz = new ReentrantClazz();
        final ReentrantLock reentrantLock =new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {

                    try {
                        counter.inc();//OrdinaryLock实例
                        //reentrantClazz.outer();//重入锁实例 这是重入锁的我现在先注释掉

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
  class OrdinaryLock {
    private boolean isLocked = false;

    public synchronized void lock() throws InterruptedException {
        while (isLocked) {    //不用if,而用while,是为了防止假唤醒
            wait();
        }
        isLocked = true;
    }

    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}

如上代码,我先把inc()的lock注释掉;运行结果如下:

控制台打印无序,而且前后相差不止1;具体逻辑看inc()方法.

12前...0
16前...0
20前...1
17前...0
17后...4
18前...0
15前...0
14前...0
13前...0
11前...0
13后...8
14后...7
15后...6
18后...5
20后...3
16后...2
19前...1
19后...10
12后...1
11后...9

Process finished with exit code 0

当我加了普通锁之后,把注释放开,就会有序,而且保证线程加减前后相差是1.运行结果如下所示

11前...0
11后...1
13前...1
13后...2
14前...2
14后...3
12前...3
12后...4
15前...4
15后...5
16前...5
16后...6
17前...6
17后...7
19前...7
19后...8
18前...8
18后...9
20前...9
20后...10

Process finished with exit code 0

这就是加锁的魅力,但是同时也会损失效率和响应速度.

重入锁

重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。 重入锁的实现方式:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

接下来先演示没有重入特性的普通锁,当内部方法想要获取锁的时候就会陷入死锁.

代码如下,在Counter.java里面新增内部类,然后在main()方法中调用这个类的outer方法.outer()调用inner(),inner()也尝试获取锁.结果会陷入死锁

synchronized:可重入锁; java.util.concurrent.locks.ReentrantLock:可重入锁;

//重入示例,outer调用inner 方法
//lock可以使用origin lock 或者reentrant lock
//使用origin lock 会造成死锁 reentrant lock 不会
class ReentrantClazz {
    OrdinaryLock lock = new OrdinaryLock();//:TODO 修改为reentrantLock 再运行即可

    public void outer() throws InterruptedException {
        lock.lock();
        System.out.println("进入outter");
        inner();
        lock.unlock();
    }

    public void inner() throws InterruptedException {
        System.out.println("进入inner");
        lock.lock();
        //do something
        lock.unlock();
    }
}

运行结果如下,

没有结束,只是获取并没有释放,因为上面使用的lock是OrdinaryLock,接下来把这个换成可重入锁ReentrantLock,

ReentrantClazz类中的OrdinaryLock lock = new OrdinaryLock();,换成ReentrantLock lock = new ReentrantLock();

//可重入的锁
class ReentrantLock {
    private boolean isLocked = false;
    private Thread lockedBy = null;
    private int lockedCount = 0;

    public synchronized void lock()
            throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (isLocked && lockedBy != callingThread) {
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
    }

    public synchronized void unlock() {
        if (Thread.currentThread() == this.lockedBy) {
            lockedCount--;
            if (lockedCount == 0) {
                isLocked = false;
                notify();
            }
        }
    }
}

运行结果如下:

11进入outter
11线程outer()方法获取了锁
11进入inner
11线程inner()方法获取了锁
11线程inner()方法释放了锁
11线程outer()方法释放了锁
12进入outter
12线程outer()方法获取了锁
12进入inner
12线程inner()方法获取了锁
12线程inner()方法释放了锁
12线程outer()方法释放了锁
13进入outter
13线程outer()方法获取了锁
13进入inner
13线程inner()方法获取了锁
13线程inner()方法释放了锁
13线程outer()方法释放了锁
14进入outter
14线程outer()方法获取了锁
14进入inner
14线程inner()方法获取了锁
14线程inner()方法释放了锁
14线程outer()方法释放了锁
15进入outter
15线程outer()方法获取了锁
15进入inner
15线程inner()方法获取了锁
15线程inner()方法释放了锁
15线程outer()方法释放了锁
16进入outter
16线程outer()方法获取了锁
16进入inner
16线程inner()方法获取了锁
16线程inner()方法释放了锁
16线程outer()方法释放了锁
17进入outter
17线程outer()方法获取了锁
17进入inner
17线程inner()方法获取了锁
17线程inner()方法释放了锁
17线程outer()方法释放了锁
18进入outter
18线程outer()方法获取了锁
18进入inner
18线程inner()方法获取了锁
18线程inner()方法释放了锁
18线程outer()方法释放了锁
19进入outter
19线程outer()方法获取了锁
19进入inner
19线程inner()方法获取了锁
19线程inner()方法释放了锁
19线程outer()方法释放了锁
20进入outter
20线程outer()方法获取了锁
20进入inner
20线程inner()方法获取了锁
20线程inner()方法释放了锁
20线程outer()方法释放了锁

Process finished with exit code 0

很明显每个线程的outer和inner都能获取并释放锁.这就是可重入锁.

GitHub代码地址:https://github.com/1344115844/learning

自旋锁

自旋锁的核心:不放弃时间片。线程获取不到锁,就会被阻塞挂起,等其他线程释放锁的时候,才被唤醒起来。线程挂起和唤醒是需要转入到内核态完成的,这些操作对系统的并发性能会带来影响。其实有时候线程虽然没法立刻获取到锁,但是也可能很快就会获取到锁。JVM采用了一种叫自旋锁的机制,让获取不到锁的线程执行一个空的循环,一段时间后,如果还是没法获取锁,线程才会被挂起。 如果锁竞争不严重的情况下,且任务执行时间不长,那么可以尝试使用自旋锁。

自旋锁可能引起的问题:

  1. 过多占据CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞;
  2. 死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

代码实现自旋锁的不可重入锁:

BadSpinLock.java

/**
 * @author Veng Su 1344114844@qq.com
 * @date 2018/6/18 8:49
 */

import java.util.concurrent.atomic.AtomicReference;
/**
 *@author Veng Su 2018/6/18 8:53
 *不可重入的自旋锁
 **/

public class BadSpinLock {

    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象
    public void lock() {
        Thread cur = Thread.currentThread();
        while (!owner.compareAndSet(null, cur)) {
            System.out.println(cur.getId()+ " 自旋中");
        }
        System.out.println(cur.getId()+"线程上锁成功");
    }

    public void unLock() {
        Thread cur = Thread.currentThread();
        if (cur == owner.get()) {
            owner.compareAndSet(cur, null);
            System.out.println(cur.getId()+ " 释放了锁");
        }
    }


}

这里是没有使用count进行线程的获取锁的计数.会陷入死锁

main方法如下:

public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        final ReentrantClazz reentrantClazz = new ReentrantClazz();
        final ReentrantLock reentrantLock =new ReentrantLock();
        final SpinLock spinLock =new SpinLock();
        for (int i = 0; i < 1; i++) {
            new Thread(new Runnable() {
                public void run() {

                    try {
//                        counter.inc();//OrdinaryLock实例
                        reentrantClazz.outer();//重入锁实例
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    
//重入示例,outer调用inner 方法
//lock可以使用origin lock 或者reentrant lock
//使用origin lock 会造成死锁 reentrant lock 不会
class ReentrantClazz {
    BadSpinLock lock = new BadSpinLock();//:TODO 修改为BadSpinLock 再运行即可

    public void outer() throws InterruptedException {
        System.out.println(Thread.currentThread().getId()+"进入outter");
        lock.lock();
        System.out.println(Thread.currentThread().getId()+"线程outer()方法获取了锁");
        inner();
        lock.unLock();
        System.out.println(Thread.currentThread().getId()+"线程outer()方法释放了锁");

    }

    public void inner() throws InterruptedException {
        System.out.println(Thread.currentThread().getId()+"进入inner");
        lock.lock();
        System.out.println(Thread.currentThread().getId()+"线程inner()方法获取了锁");
        //do something
        lock.unLock();
        System.out.println(Thread.currentThread().getId()+"线程inner()方法释放了锁");
    }
}

运行结果如下:

进入inner方法后就获取不到锁了,这是不可重入锁,造成死锁.

代码实现自旋锁的可重入锁

SpinLock.java

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Veng Su 1344114844@qq.com
 * @date 2018/6/18 8:35
 * 可重入的自旋锁
 */
public class SpinLock {
    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象
    private int count;//用一个计数器 来做 重入锁获取次数的计数
    public void lock() {
        Thread cur = Thread.currentThread();
        if (cur == owner.get()) {
            count++;
            return;
        }

        while (!owner.compareAndSet(null, cur)) {//当线程越来越多  由于while循环 会浪费CPU时间片,CompareAndSet 需要多次对同一内存进行访问
            //会造成内存的竞争,然而对于X86,会采取竞争内存总线的方式来访问内存,所以会造成内存访问速度下降(其他线程老访问缓存),因而会影响整个系统的性能
            System.out.println(Thread.currentThread().getId()+"线程自旋中....");
        }
    }

    public void unLock() {
        Thread cur = Thread.currentThread();
        if (cur == owner.get()) {
            if (count > 0) {
                count--;
            } else {
                owner.compareAndSet(cur, null);
            }
        }
    }
}

然后把reentrantClazz类的lock换成spinlock;

运行结果如下:

11进入outter
11线程outer()方法获取了锁
11进入inner
11线程inner()方法获取了锁
11线程inner()方法释放了锁
11线程outer()方法释放了锁
12进入outter
12线程outer()方法获取了锁
12进入inner
12线程inner()方法获取了锁
12线程inner()方法释放了锁
12线程outer()方法释放了锁
13进入outter
13线程outer()方法获取了锁
13进入inner
13线程inner()方法获取了锁
13线程inner()方法释放了锁
13线程outer()方法释放了锁
14进入outter
14线程outer()方法获取了锁
14进入inner
14线程inner()方法获取了锁
14线程inner()方法释放了锁
14线程outer()方法释放了锁
16进入outter
16线程outer()方法获取了锁
16进入inner
16线程inner()方法获取了锁
16线程inner()方法释放了锁
16线程outer()方法释放了锁
17进入outter
17线程outer()方法获取了锁
17进入inner
17线程inner()方法获取了锁
17线程inner()方法释放了锁
18进入outter
17线程outer()方法释放了锁
18线程outer()方法获取了锁
18进入inner
18线程inner()方法获取了锁
18线程inner()方法释放了锁
18线程outer()方法释放了锁
19进入outter
19线程outer()方法获取了锁
19进入inner
19线程inner()方法获取了锁
19线程inner()方法释放了锁
19线程outer()方法释放了锁
20进入outter
20线程outer()方法获取了锁
20进入inner
20线程inner()方法获取了锁
20线程inner()方法释放了锁
20线程outer()方法释放了锁
15进入outter
15线程outer()方法获取了锁
15进入inner
15线程inner()方法获取了锁
15线程inner()方法释放了锁
15线程outer()方法释放了锁

Process finished with exit code 0

公平锁和非公平锁

ReentrantLock锁的实现分析

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}12345
  • tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。
  • addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值而非公平锁在对于新晋线程有很大优势
  • acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。
  • selfInterrupt 如果线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。

ReentrantLock 对线程的阻塞是基于 LockSupport.park(this); (见 AbstractQueuedSynchronizer#parkAndCheckInterrupt)。 先决条件是当前节点有限次尝试获取锁失败。

公平锁和非公平锁在说的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。 但是当并发情况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可通过CPU对共享变量加锁的形式,实现数据变更的原子操作。 volatile 和 CAS的结合是并发抢占的关键。

公平锁FairSync

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:

if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}12345

其中hasQueuedPredecessors是用于检查是否有等待队列的。

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }1234567

非公平锁NonfairSync

非公平锁在实现的时候多次强调随机抢占:

if (c == 0) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}123456

与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁。如果被加入了等待队列后则跟公平锁没有区别。

ReentrantLock锁的释放

ReentrantLock锁的释放是逐级释放的,也就是说在 可重入性 场景中,必须要等到场景内所有的加锁的方法都释放了锁, 当前线程持有的锁才会被释放! 释放的方式很简单, state字段减一即可:

protected final boolean tryRelease(int releases) {
    //  releases = 1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}12345678910111213

ReentrantLock等待队列中元素的唤醒

当当前拥有锁的线程释放锁之后, 且非公平锁无线程抢占,就开始线程唤醒的流程。 通过tryRelease释放锁成功,调用LockSupport.unpark(s.thread); 终止线程阻塞。 见代码:

private void unparkSuccessor(Node node) {
    // 强行回写将被唤醒线程的状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    // s为h的下一个Node, 一般情况下都是非Null的
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 否则按照FIFO原则寻找最先入队列的并且没有被Cancel的Node
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 再唤醒它
    if (s != null)
        LockSupport.unpark(s.thread);
}123456789101112131415161718

ReentrantLock内存可见性分析

针对如下代码:

try {
    lock.lock();
    i ++;
} finally {
    lock.unlock();
}123456

可以发现哪怕在不使用 volatile关键字修饰元素i的时候, 这里的i 也是没有并发问题的。

互斥锁

保证在同一时刻只有一个线程对其进行操作。比如最常见的 synchronized。

参考文献

Java中的公平锁和非公平锁实现详解

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券