打通Java任督二脉并发数据结构

老大难的JavaClassLoader,到了该彻底理解它的时候了

每一个Java的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java内置的锁是如何实现的?最常用的最简单的锁要数ReentrantLock,使用它加锁时如果没有立即加成功,就会阻塞当前的线程等待其它线程释放锁之后再重新尝试加锁,那线程是如何实现阻塞自己的?其它线程释放锁之后又是如果唤醒当前线程的?当前线程是如何得出自己没有加锁成功这一结论的?本篇内容将会从根源上回答上面提到的所有问题

线程阻塞原语

Java的线程阻塞和唤醒是通过Unsafe类的park和unpark方法做到的。

publicclassUnsafe{...publicnativevoidpark(booleanisAbsolute,longtime);publicnativevoidunpark(Threadt);...}

这两个方法都是native方法,它们本身是由C语言来实现的核心功能。park的意思是停车,让当前运行的线程Thread.currentThread()休眠,unpark的意思是解除停车,唤醒指定线程。这两个方法在底层是使用操作系统提供的信号量机制来实现的。具体实现过程要深究C代码,这里暂时不去具体分析。park方法的两个参数用来控制休眠多长时间,第一个参数isAbsolute表示第二个参数是绝对时间还是相对时间,单位是毫秒。

线程从启动开始就会一直跑,除了操作系统的任务调度策略外,它只有在调用park的时候才会暂停运行。锁可以暂停线程的奥秘所在正是因为锁在底层调用了park方法。

parkBlocker

线程对象Thread里面有一个重要的属性parkBlocker,它保存当前线程因为什么而park。就好比停车场上停了很多车,这些车主都是来参加一场拍卖会的,等拍下自己想要的物品后,就把车开走。那么这里的parkBlocker大约就是指这场「拍卖会」。它是一系列冲突线程的管理者协调者,哪个线程该休眠该唤醒都是由它来控制的。

classThread{...volatileObjectparkBlocker;...}

当线程被unpark唤醒后,这个属性会被置为null。Unsafe.park和unpark并不会帮我们设置parkBlocker属性,负责管理这个属性的工具类是LockSupport,它对Unsafe这两个方法进行了简单的包装。

classLockSupport{...publicstaticvoidpark(Objectblocker){Threadt=Thread.currentThread();setBlocker(t,blocker);U.park(false,0L);setBlocker(t,null);//醒来后置null}publicstaticvoidunpark(Threadthread){if(thread!=null)U.unpark(thread);}}...}

Java的锁数据结构正是通过调用LockSupport来实现休眠与唤醒的。线程对象里面的parkBlocker字段的值就是下面我们要讲的「排队管理器」。

排队管理器

当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。每一把锁内部都会有这样一个队列管理器,管理器里面会维护一个等待的线程队列。ReentrantLock里面的队列管理器是AbstractQueuedSynchronizer,它内部的等待队列是一个双向列表结构,列表中的每个节点的结构如下。

classAbstractQueuedSynchronizer{volatileNodehead;//队头线程将优先获得锁volatileNodetail;//抢锁失败的线程追加到队尾volatileintstate;//锁计数}classNode{Nodeprev;Nodenext;Threadthread;//每个节点一个线程//下面这两个特殊字段可以先不去理解NodenextWaiter;//请求的是共享锁还是独占锁intwaitStatus;//精细状态描述字}

加锁不成功时,当前的线程就会把自己纳入到等待链表的尾部,然后调用LockSupport.park将自己休眠。其它线程解锁时,会从链表的表头取一个节点,调用LockSupport.unpark唤醒它。

图片

AbstractQueuedSynchronizer类是一个抽象类,它是所有的锁队列管理器的父类,JDK中的各种形式的锁其内部的队列管理器都继承了这个类,它是Java并发世界的核心基石。比如ReentrantLock、ReadWriteLock、CountDownLatch、Semaphone、ThreadPoolExecutor内部的队列管理器都是它的子类。这个抽象类暴露了一些抽象方法,每一种锁都需要对这个管理器进行定制。而JDK内置的所有并发数据结构都是在这些锁的保护下完成的,它是JDK多线程高楼大厦的地基。

图片

锁管理器维护的只是一个普通的双向列表形式的队列,这个数据结构很简单,但是仔细维护起来却相当复杂,因为它需要精细考虑多线程并发问题,每一行代码都写的无比小心。

JDK锁管理器的实现者是DouglasS.Lea,Java并发包几乎全是他单枪匹马写出来的,在算法的世界里越是精巧的东西越是适合一个人来做。

DouglasS.Lea是纽约州立大学奥斯威戈分校计算机科学教授和现任计算机科学系主任,专门研究并发编程和并发数据结构的设计。他是JavaCommunityProcess的执行委员会成员,主持JSR,它为Java编程语言添加了并发实用程序。

图片

后面我们将AbstractQueuedSynchronizer简写成AQS。我必须提醒各位读者,AQS太复杂了,如果在理解它的路上遇到了挫折,这很正常。目前市场上并不存在一本可以轻松理解AQS的书籍,能够吃透AQS的人太少太少,我自己也不算。

公平锁与非公平锁

公平锁会确保请求锁和获得锁的顺序,如果在某个点锁正处于自由状态,这时有一个线程要尝试加锁,公平锁还必须查看当前有没有其它线程排在排队,而非公平锁可以直接插队。联想一下在肯德基买汉堡时的排队场景。

也许你会问,如果某个锁处于自由状态,那它怎么会有排队的线程呢?我们假设此刻持有锁的线程刚刚释放了锁,它唤醒了等待队列中第一个节点线程,这时候被唤醒的线程刚刚从park方法返回,接下来它就会尝试去加锁,那么从park返回到加锁之间的状态就是锁的自由态,这很短暂,而这短暂的时间内还可能有其它线程也在尝试加锁。

其次还有一点需要注意,执行了Lock.park方法的线程自我休眠后,并不是非要等到其它线程unpark了自己才会醒来,它可能随时会以某种未知的原因醒来。我们看源码注释,park返回的原因有四种

其它线程unpark了当前线程

时间到了自然醒(park有时间参数)

其它线程interrupt了当前线程

其它未知原因导致的「假醒」

文档中没有明确说明何种未知原因会导致假醒,它倒是说明了当park方法返回时并不意味着锁自由了,醒过来的线程在重新尝试获取锁失败后将会再次park自己。所以加锁的过程需要写在一个循环里,在成功拿到锁之前可能会进行多次尝试。

计算机世界非公平锁的服务效率要高于公平锁,所以Java默认的锁都使用了非公平锁。不过现实世界似乎非公平锁的效率会差一点,比如在肯德基如果可以不停插队,你可以想象现场肯定一片混乱。为什么计算机世界和现实世界会有差异,大概是因为在计算机世界里某个线程插队并不会导致其它线程抱怨。

publicReentrantLock(){this.sync=newNonfairSync();}publicReentrantLock(booleanfair){this.sync=fair?newFairSync():newNonfairSync();}共享锁与排他锁

ReentrantLock的锁是排他锁,一个线程持有,其它线程都必须等待。而ReadWriteLock里面的读锁不是排他锁,它允许多线程同时持有读锁,这是共享锁。共享锁和排他锁是通过Node类里面的nextWaiter字段区分的。

classAQS{staticfinalNodeSHARED=newNode();staticfinalNodeEXCLUSIVE=null;booleanisShared(){returnthis.nextWaiter==SHARED;}}

那为什么这个字段没有命名成mode或者type或者干脆直接叫shared?这是因为nextWaiter在其它场景还有不一样的用途,它就像C语言联合类型的字段一样随机应变,只不过Java语言没有联合类型。

条件变量

关于条件变量,需要提出的第一个问题是为什么需要条件变量,只有锁还不够么?考虑下面的伪代码,当某个条件满足时,才去干某件事

voiddoSomething(){locker.lock();while(!condition_is_true()){//先看能不能搞事locker.unlock();//搞不了就歇会再看看能不能搞sleep(1);locker.lock();//搞事需要加锁,判断能不能搞事也需要加锁}justdoit();//搞事locker.unlock();}

当条件不满足时,就循环重试(其它线程会通过加锁来修改条件),但是需要间隔sleep,不然CPU就会因为空转而飙高。这里存在一个问题,那就是sleep多久不好控制。间隔太久,会拖慢整体效率,甚至会错过时机(条件瞬间满足了又立即被重置了),间隔太短,又回导致CPU空转。有了条件变量,这个问题就可以解决了

voiddoSomethingWithCondition(){cond=locker.newCondition();locker.lock();while(!condition_is_true()){cond.await();}justdoit();locker.unlock();}

await()方法会一直阻塞在cond条件变量上直到被另外一个线程调用了cond.signal()或者cond.signalAll()方法后才会返回,await()阻塞时会自动释放当前线程持有的锁,await()被唤醒后会再次尝试持有锁(可能又需要排队),拿到锁成功之后await()方法才能成功返回。

图片

阻塞在条件变量上的线程可以有多个,这些阻塞线程会被串联成一个条件等待队列。当signalAll()被调用时,会唤醒所有的阻塞线程,让所有的阻塞线程重新开始争抢锁。如果调用的是signal()只会唤醒队列头部的线程,这样可以避免「惊群问题」。

await()方法必须立即释放锁,否则临界区状态就不能被其它线程修改,condition_is_true()返回的结果也就不会改变。这也是为什么条件变量必须由锁对象来创建,条件变量需要持有锁对象的引用这样才可以释放锁以及被signal唤醒后重新加锁。创建条件变量的锁必须是排他锁,如果是共享锁被await()方法释放了并不能保证临界区的状态可以被其它线程来修改,可以修改临界区状态的只能是排他锁。这也是为什么ReadWriteLock.ReadLock类的newCondition方法定义如下

publicConditionnewCondition(){thrownewUnsupportedOperationException();}

有了条件变量,sleep不好控制的问题就解决了。当条件满足时,调用signal()或者signalAll()方法,阻塞的线程可以立即被唤醒,几乎没有任何延迟。

条件等待队列

当多个线程await()在同一个条件变量上时,会形成一个条件等待队列。同一个锁可以创建多个条件变量,就会存在多个条件等待队列。这个队列和AQS的队列结构很接近,只不过它不是双向队列,而是单向队列。队列中的节点和AQS等待队列的节点是同一个类,但是节点指针不是prev和next,而是nextWaiter。

classAQS{...classConditionObject{NodefirstWaiter;//指向第一个节点NodelastWaiter;//指向第二个节点}classNode{staticfinalintCONDITION=-2;staticfinalintSIGNAL=-1;Threadthread;//当前等待的线程NodenextWaiter;//指向下一个条件等待节点Nodeprev;Nodenext;intwaitStatus;//waitStatus=CONDITION}...}图片

ConditionObject是AQS的内部类,这个对象里会有一个隐藏的指针this$0指向外部的AQS对象,ConditionObject可以直接访问AQS对象的所有属性和方法(加锁解锁)。位于条件等待队列里的所有节点的waitStatus状态都被标记为CONDITION,表示节点是因为条件变量而等待。

队列转移

当条件变量的signal()方法被调用时,条件等待队列的头节点线程会被唤醒,该节点从条件等待队列中被摘走,然后被转移到AQS的等待队列中,准备排队尝试重新获取锁。这时节点的状态从CONDITION转为SIGNAL,表示当前节点是被条件变量唤醒转移过来的。

classAQS{...booleantransferForSignal(Nodenode){//重置节点状态if(!node.







































北京白癜风专科医院怎么走
白癜风治疗中药有什么



转载请注明:http://www.92nongye.com/txjg/204621523.html