当前位置: 首页 > 后端技术 > Java

自己写LockSupport60行是什么体验

时间:2023-04-01 19:57:58 Java

自己写LockSupport60行是什么体验?前言在JDK提供给我们的各种并发工具中,比如ReentrantLock等工具的内部实现,经常会用到一个工具,这个工具就是LockSupport。LockSupport为我们提供了一个非常强大的功能。它是线程阻塞的最基本原语。它可以阻塞或唤醒一个线程,所以常用于并发场景。LockSupport实现原理在了解LockSupport实现原理之前,我们先通过一个案例来了解LockSupport的功能!importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.LockSupport;publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{System.out.println("beforepark");LockSupport.park();//park函数可以挂起调用该方法的线程System.out.println("afterpark");});thread.start();TimeUnit.SECONDS.sleep(5);System.out.println("主线程休息了5s");System.out.println("主线程unpark线程");LockSupport.unpark(线程);//主线程会在thread线程被唤醒后,thread线程才能继续执行}}上面代码输出结果如下:park前,主线程休息5s,主线程unpark主线程。功能好像一样,其实不然。让我们看下面的代码:importjava.util.concurrent.TimeUnit;导入java.util.concurrent.locks.LockSupport;publicclassDemo02{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread=newThread(()->{try{TimeUnit.SECONDS.sleep(5);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("beforepark");LockSupport。park();//在线程threadSystem.out.println("afterpark")之后停放操作;});thread.start();System.out.println("主线程unpark线程");LockSupport.unpark(线程);//先执行unpark操作}}上面代码输出结果如下:主线程unpark先于threadpark,上面代码之后,主线程先执行unpark操作,thread线程再执行park手术。这样的话程序也可以正常执行但是如果signal调用在await调用之前,程序就不会执行,比如下面的代码:importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassDemo03{privatestaticfinalReentrantLocklock=newReentrantLock();privatestaticfinalCondition条件=lock.newCondition();publicstaticvoidthread()throwsInterruptedException{lock.lock();尝试{TimeUnit.SECONDS.sleep(5);条件.await();System.out.println("等待完成");}最后{lock.unlock();}}publicstaticvoidmainThread(){lock.lock();try{System.out.println("发送信号");条件.信号();}最后{lock.unlock();System.out.println("主线程解锁完成");}}publicstaticvoidmain(String[]args){Threadthread=newThread(()->{try{thread();}catch(InterruptedExceptione){e.printStackTrace();}});thread.start();主线程();}}上面代码的输出结果如下:sendsignal主线程解锁完成上面代码中永远不会打印出“waitingforcompletion”,这是因为signal函数的调用是在await之前,signal函数只会影响在它之前执行的await函数,不会影响在它之后调用的await。那么是什么造成了这种影响呢?实际上,JVM在实现LockSupport时,内部会为每个线程维护一个计数器变量_counter。此变量表示“许可证数”。只有有许可证,线程才能执行。同时,license的数量最多只能是1个,调用一次park,license的数量就会减1。unpark调用一次,计数器会加1,但是计数器的值不能超过1。线程调用park时,需要等待license。只有获得许可后线程才能继续执行,或者如果在park之前已经获得了许可,那么就不需要阻塞,可以直接执行。.实现自己的LockSupport实现原理在上一篇文章中,我们已经介绍了locksupport的原理。它的内部主要实现是通过licenses来实现的:每个线程最多可以获得1个licenses,当调用unpark方法时,线程可以获得一个license。License数量上限为1个,如果已经有一个License,则不能累积License。在调用park方法时,如果调用park方法的线程没有license,则需要挂起该线程,直到另一个线程调用unpark方法给该线程颁发license,该线程才能继续执行。但是如果线程已经有了permit,那么线程就不会阻塞,可以直接执行。自己实现的LockSupport协议规定,在我们自己实现的Parker中,也可以给每个线程一个计数器,用来记录线程许可的数量。当许可数大于等于0时,线程才能执行,否则需要阻塞线程,协议的具体规则如下:初始线程的许可数为0。如果我们调用park时,计数器的值等于1,计数器的值变为0,则线程可以继续执行。如果我们调用park的时候计数器的值等于0,线程就不能继续执行,需要挂起线程,将计数器的值设置为-1。如果我们调用unpark时unparked线程的计数器的值等于0,我们需要将计数器的值改为1。如果我们调用unpark时unparked线程的计数器的值等于1,那么不需要改变计数器的值,因为计数器的最大值是1。当我们调用unpark时,如果计数器等于-1,表示线程已经挂起,需要唤醒线程,同时需要将计数器的值置0。因为工具涉及线程阻塞和唤醒,我们可以使用可重入锁ReentrantLock和条件变量Condition,所以需要熟悉这两个工具的使用。ReentrantLock主要用于加锁和解锁,保护临界区。Condition.awat方法用于阻塞线程。Condition.signal方法用于唤醒线程。因为我们需要在unpark方法中传入一个具体的线程,给这个线程颁发license,同时唤醒这个线程,因为需要对具体的线程进行唤醒,而被唤醒的线程条件是不确定的,所以我们需要为每个线程设置它。一个线程维护一个计数器和条件变量,这样每个条件变量只与一个线程相关,而且必须是特定的线程才能被唤醒。我们可以用HashMap来实现,key是线程,value是计数器或者条件变量。具体实现因此,根据以上分析,我们的类变量如下:privatefinalReentrantLock锁;//用于保护关键访问privatefinalHashMappermits;//允许的数量privatefinalHashMapconditions;//用于唤醒和阻塞线程的条件变量构造函数,主要是给变量赋值:publicParker(){lock=newReentrantLock();permits=newHashMap<>();条件=新的HashMap<>();}park方法publicvoidpark(){Threadt=Thread.currentThread();//先获取当前正在执行的线程if(conditions.get(t)==null){//如果没有条件对应线程就创建conditions.put(t,lock.newCondition());}锁.锁();try{//如果没有创建license变量或者license等于0,说明没有license,需要挂起线程if(permits.get(t)==null||permits.get(t)==0){permits.put(t,-1);//同时允许的数量应该设置为-1conditions.get(t).await();}elseif(permits.get(t)>0){permits.put(t,0);//如果permits的个数大于0,即1,说明线程已经有permit所以可以直接释放但是需要消耗license}}catch(InterruptedExceptione){e.printStackTrace();}最后{lock.unlock();}}unpark方法publicvoidunpark(Threadthread){线程t=线程;//给线程threadlock.lock()颁发许可;try{if(permits.get(t)==null)//如果permit变量还没有创建线程当前的许可数等于初始值0所以方法permit之后的许可数是1permits.put(t,1);elseif(permits.get(t)==-1){//if如果permits个数为-1,说明thread线程已经调用了park方法,thread线程已经被挂起。所以在unpark函数中把permits的个数设置为0并不急,唤醒线程permits.put(t,0)也是必要的;条件.get(t).signal();}elseif(permits.get(t)==0){//如果permits个数为0,说明线程正在执行,所以permits个数加1permits.put(t,1);}//除此之外,permit为1,此时不需要操作,因为最大permit数为1}finally{lock.unlock();}}完整代码importjava.util.HashMap;importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.ReentrantLock;publicclassParker{privatefinalHashMap许可;privatefinalHashMap条件;publicParker(){lock=newReentrantLock();permits=newHashMap<>();ons=newHashMap<>();}publicvoidpark(){Threadt=Thread.currentThread();if(conditions.get(t)==null){conditions.put(t,lock.newCondition());}锁.锁();尝试{if(permits.get(t)==null||permits.get(t)==0){permits.put(t,-1);条件.get(t).await();}elseif(permits.get(t)>0){permits.put(t,0);}}catch(InterruptedExceptione){e.printStackTrace();}最后{lock.unlock();}}publicvoidunpark(Threadthread){Threadt=thread;锁.锁();尝试{如果(permits.get(t)==null)permits.put(t,1);elseif(permits.get(t)==-1){permits.put(t,0);条件.get(t).signal();}elseif(permits.get(t)==0){permits.put(t,1);}}最后{lock.unlock();JVM实现一览其实JVM底层的park和unpark的实现也是基于锁和条件变量,只不过它使用了更底层的操作系统和libc(linux提供的API)操作系统)已实施虽然API不同,但原理相似,思路相似。例如下面是JVM实现的unpark方法:voidParker::unpark(){ints,status;//加锁操作等同于可重入锁的lock.lock()status=pthread_mutex_lock(_mutex);assert(status==0,"不变");s=_计数器;_counter=1;if(s<1){//如果license小于1,执行下面的操作if(WorkAroundNPTLTimedWaitHang){//这行代码相当于condition。signal()唤醒线程status=pthread_cond_signal(_cond);断言(状态==0,“不变”);//解锁操作等同于可重入锁lock.unlock()status=pthread_mutex_unlock(_mutex);断言(状态==0,“不变”);}else{status=pthread_mutex_unlock(_mutex);断言(状态==0,“不变”);status=pthread_cond_signal(_cond);断言(状态==0,“不变”);}}else{//如果有license,即s==1,则线程不允许被挂起//解锁操作相当于一个可重入锁的lock.unlock()pthread_mutex_unlock(_mutex);断言(状态==0,“不变”);}}JVM实现的park方法,如果没有license也会挂起线程:总结主要在本文介绍介绍锁支持的用法和一般原理,介绍我们如何实现类似锁支持的功能,定义自己的通用协议来实现锁支持。整个过程比较清楚。我们刚才实现的锁支持有两个核心方法,其他的方法其实都差不多,原理也差不多,这里就实现一个乞丐版的锁支持!!!使用锁和条件变量来阻塞和唤醒线程使用Thread.currentThread()方法获取当前正在执行的线程。使用HashMap来存储线程与许可证和条件变量之间的关系。以上就是本文的全部内容,我是LeHung,我们下期再见!!!更多精彩内容合集可以访问项目:https://github.com/Chang-LeHu...关注公众号:一个没用的研究僧,学习更多计算机知识(Java,Python,计算机系统基础,算法和数据结构)知识。