深入理解并发编程之Lock源码分析

深入理解并发编程之Lock源码分析


一、Lock简要分析

Java中我们常用的锁就是Synchronized关键字和Lock锁,Synchronized是由C++写的,我们在前面已经分析了,Lock锁是使用Java代码写的我们可以直接点击进去看代码分析,看代码我们知道:
Lock是通过AQS + LockSupport + CAS实现

AQS

AQS全称为AbstractQueuedSynchronizer 是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件。简单来说AQS就是一个队列,在Lock锁中的作用是用来存放阻塞线程的。

LockSupport

LockSupport是一个工具类,主要功能是阻塞和唤醒线程,有两个核心方法:

  • park():用来阻塞线程,可以不传参标识阻塞当前线程,也可以用线程做参数表示阻塞传递的线程。
  • unpark():用来唤醒线程,用线程做参数表示唤醒传递的线程。
    看一个简单使用代码:
public class Test001 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("start");
            LockSupport.park(Thread.currentThread());  // 让当前线程变为阻塞状态,也可以不传参数默认当前线程
            System.out.println("end" + Thread.currentThread().isInterrupted());
        });
        t1.start();
        try {
            Thread.sleep(1000); //防止代码执行过快先打印唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("唤醒阻塞的线程");
        LockSupport.unpark(t1); //唤醒刚才阻塞的thread线程
    }
}

看一下执行结果,符合预期(先执行打印唤醒然后执行打印end):
在这里插入图片描述

CAS

CAS我们在前面已经讲过了,比较交换,V(共享变量)、E(旧值)、N(新值)三个参数,先比较V==E,如果相等则把N新值赋值给V。

二、Lock源码分析

这里不细翻代码了,直接使用通过抽取Lock核心源码写的简要Lock锁来分析(当然这只是个简单的源代码示意,实际代码量是比较多的,考虑的问题比较全面操作也比较细,这个简单代码在并发量较大的情况下还是有不少漏洞的,这是做原理分析简单测试没问题的):

public class MyLock {
    /**
     * 获取锁的状态 0当前线程没有获取该锁,1 已经有线程获取到该锁
     */
    AtomicInteger state= new AtomicInteger(0);
    // 记录锁被那个线程持有
    private transient Thread exclusiveOwnerThread;
    // 存放没有获取锁到的线程
    private ConcurrentLinkedDeque<Thread> waitThreads = new ConcurrentLinkedDeque<>();

    /**
     * 获取锁
     */
    public void lock() {
        // 底层使用cas 修改锁的状态从0变为1  硬件层面帮助我们实现
        if (acquire()) {
            return;
        }

        // 使用cas 修改锁的状态失败 设计重试次数
        Thread currentThread = Thread.currentThread();
        // 如果该线程已经存在的情况下
        waitThreads.add(currentThread);
        for (; ; ) {
            //短暂重试
            if (acquire()) {
                // 移除队列
                waitThreads.push(currentThread);
                return;
            }
            // 重试一次还是没有获取到锁,将当前的这个线程变为阻塞状态
            LockSupport.park();

        }
    }

    /**
     * 释放锁
     */
    public void unLock() {
        if (exclusiveOwnerThread != Thread.currentThread()) {
            throw new RuntimeException("不是当前线程在释放锁");
        }
        // 释放锁
        if (compareAndSetState(1, 0)) {
            this.exclusiveOwnerThread = null;
            // 取出阻塞的线程 唤醒
            Thread pollThread = waitThreads.poll();
            if (pollThread != null)
                // 唤醒刚才阻塞的线程
                LockSupport.unpark(pollThread);
        }
    }

    /**
     * 通过CAS修改锁的状态
     * @return
     */
    private boolean acquire() {
        if (compareAndSetState(0, 1)) { //如果预期是0,则修改状态为1表示当前线程获取到锁
            //当前线程获取到锁
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    /**
     * CAS 三个参数  V  E
     *
     * @param expect
     * @param update
     * @return
     */
    private boolean compareAndSetState(int expect, int update) {
        return state.compareAndSet(expect, update);
    }

    /**
     *  设置获取锁的为当前线程
     * @param exclusiveOwnerThread
     */
    public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {
        this.exclusiveOwnerThread = exclusiveOwnerThread;
    }
}

首先对三个属性进行讲解:

  • state:这是标识符,用来标识当前线程是否获取到锁,0表示没有获取到锁;1表示获取到锁,源码中使用的private volatile int state,我们就简单的使用原子类来用一下。
  • exclusiveOwnerThread:用来记录当前持有锁的线程。
  • waitThreads:这是一个队列,用来存放没有获取到锁进入等待状态的线程,源码中使用的双向链表操作比较复杂,暂时用个队列类来代替一下。

下面说一些几个方法:

  • lock()/unLock():这两个方法不用说了,上锁和解锁。
  • acquire():这是CAS比较方法,里面核心compareAndSetState(0,1)有两个参数,预期值和更新值,意思是如果预期是0,我们修改为1。并且返回操作是否成功。
  • setExclusiveOwnerThread():设置持有锁标记的线程。

下面重点对lock()和unlock()进行下讲解:

lock():

  1. 进入方法首先使用CAS操作来修改锁的状态,如果修改成功,则设置当前当前线程持有锁标记;
  2. 如果修改失败,说明锁标记可能被其他线程持有,将当前线程放入等待池,然后再进行CAS判断重试。
  3. 如果几次(这里for循环可以自己设置重试次数)修改后还是失败,阻塞当前线程,重试几次中有一次操作成功,则设置当前当前线程持有锁标记,并且从等待池中删除当前线程。
    这里有个需要注意的问题,当前线程放进等待池需要放在for循环前面,因为后面如果失败则进入阻塞状态了不会进行任何代码操作了

unlock():
5. 进入方法首先判断是否是持有锁标记的线程来释放锁标记的,如果其他线程释放锁标记直接抛出异常。
6. 然后释放锁,将锁的状态从1修改到0,表示当前锁空闲了。
7. 最后从锁池中取出一个锁唤醒,当然这里直接唤醒了第一个,是个公平锁,如果唤醒所有的线程则是非公平锁。

还是用卖票来实验一下:

public class Thead001 extends Thread {

    private static int count = 100;
    private MyLock lock = new MyLock();
    @Override
    public void run() {
        while (count>0){
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ",正在出票,第"+(100-count+1)+"张");
            count --;
            System.out.println("剩余票数"+count);
            lock.unLock();
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thead001();
        new Thread(thread, "窗口1").start();
        new Thread(thread, "窗口2").start();
        new Thread(thread, "窗口3").start();
        new Thread(thread, "窗口4").start();
        new Thread(thread, "窗口5").start();
    }
}

没加锁的情况下出现了超卖现象:
在这里插入图片描述
用我们写的简单锁卖票正常:
在这里插入图片描述
内容来源:蚂蚁课堂添加链接描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 《并发编程实战》是一本经典的并发编程书籍,其中包含了丰富的代码示例。这本书的代码示例非常有实战意义,可以帮助开发者在处理并发编程中的各种问题时提供参考。其中的代码示例主要涉及线程池、CAS、原子操作、锁、并发容器、BlockingQueue、CyclicBarrier和Semaphore等相关知识点。 本书的代码示例分布在各个章节中,开发者可以根据需要选择不同的示例进行学习和实践。例如,在线程池相关章节,作者提供了诸如ThreadPoolExecutor、ExecutorCompletionService等类的实现,并且提供了基于可扩展的ThreadPoolExecutor来实现动态调节线程池大小的代码示例。这些示例可以帮助开发者深入了解线程池的实现方式,以及如何进行线程池的调优。 在锁相关章节,作者提供了诸如ReentrantLock和读写锁ReentrantReadWriteLock等类的实现,并且提供了一些实际应用场景下的代码示例,例如票务系统和登录系统。这些示例可以帮助开发者了解锁的原理及其使用方法。 本书同时也介绍了一些常用的并发容器,例如ConcurrentHashMap、ConcurrentLinkedQueue等,在使用这些容器时需要注意线程安全的问题。作者为这些容器提供了详细的使用方法和代码示例,帮助开发者了解如何高效地使用这些容器。总之,《并发编程实战》的代码示例非常有价值,具有一定参考和借鉴意义,可以帮助开发者更好地掌握并发编程知识。 ### 回答2: 《Java并发编程实战》一书的源码是该书的大部分内容的实现代码。这些代码的使用可以帮助读者更好地理解并发编程的实现方式,同时也可以成为读者自己学习并发编程的参考资料。 该书的源码包括一些经典的并发编程实现,例如线程池、锁、原子变量、阻塞队列等。这些实现具有实用性和普遍性,可以帮助读者在自己的开发中解决并发编程问题。同时,该书的源码还包括一些基于实际场景的例子,让读者可以更好地理解并发编程在实际项目开发中的应用。 在使用该书源码时,读者需要关注一些细节问题,例如多线程环境下的原子性、可见性和有序性等。同时,读者还需要学会如何调试和排查多线程程序的问题,以保证程序的正确性和稳定性。 总之,该书的源码是学习并发编程的重要工具之一,读者需要认真学习源码并结合实际项目开发进行练习。只有这样,才能真正掌握并发编程的技巧和应用。 ### 回答3: 《Java并发编程实战》是一本著名的并发编程领域的经典著作,其中的源代码涵盖了Java并发编程的多个方面,非常有学习和参考的价值。 该书中的源代码主要包括了多线程并发、线程池、ThreadLocal、锁、信号量、条件等一系列并发编程相关的实例和案例,涵盖了从最基础的并发操作到应用场景的实践。 通过学习并实践这些源代码,我们可以更好地理解并发编程的思路和原理,掌握并发编程的技能和方法,提高代码质量和性能。同时,还可以培养我们的编码思维和能力,为我们今后的编程工作和研究打下坚实的基础。 总之,《Java并发编程实战》的源代码是具有非常实用和价值的,并发编程相关领域学习者和从业者都可以将其作为一个良好的学习和实践资源,不断探索和尝试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值