、
如果对num进行累加操作,使用10个线程,每个加1000次,最后应该是10000,但是你会发现不是10000。
当使用了synchronized再次进行累加操作。此时累加的值就是10000,这是因为synchronized能够保证每次只有一个线程进入临界区。
、
JDK中synchronized的使用是非常广泛的,例如线程安全的HashTable,HashTable保证线程安全就是在每个方法上都加上了synchronized。如下是HashTable的put和get方法。
对于HashTable来说,你发没发现一个问题就是实际上我们多个线程调用get方法的时候并不需要加锁,原因是如果不存在更改HashTable结构的操作,这样只是调用get方法但是加了synchronized。在多线程情况下并发度会大大降低。
那么有没有一种锁可以在读的时候不加锁,写的时候才加锁。同时写和写是冲突的,读和写也是冲突的,但是读和读是不会冲突的。这就是读写锁
ReentrantReadWriteLock。可以通过下面的例子看到实际上故意注释了解锁的unlock,所有的读锁还是能够进行获取资源。
写和读之间冲突,只有释放了写锁,读锁才能获取资源。下面代码只是方便演示,实际上解锁必须放在finally中,防止因为异常没有释放锁。其他线程阻塞的问题。
同样写和写也是冲突的,只有等写锁释放了其他线程才能够拿到写锁。
读锁的lock源码
同首先调用lock会调用acquireShared方法,而acquireShared方法中调用了tryAcquireShared(尝试获取锁),只要获取成功了就返回1否则-1,也就是说只有获取锁失败才会执行doAcquireShared。
tryAcquireShared实现如下,需要注意的是读写锁采用了state来表示状态,高16位表示读状态数量,低16位表示写状态数量。
只要获取成功就会返回1也就是不会执行doAcquireShared,也只有获取失败才会执行doAcquireShared。doAcquireShared的基本实现如下。和之前说的ReentrantLock一样,读写锁会把需要阻塞的线程放入队列中。然后当可以获取锁的时候再进行唤醒操作。
读锁的unLock源码
首先调用releaseShared方法,然后releaseShared调用了tryReleaseShared方法,只有tryReleaseShared返回为true才会调用doReleaseShared方法,同时tryReleaseShared返回true表示锁已经释放完毕,包括重入的锁。
tryReleaseShared的实现如下,可以看到只有nextc == 0的时候也就是所有读线程已经释放完毕的时候才会返回true。
doReleaseShared可能你也猜到了需要干嘛,对于阻塞的线程我们都放在队列中,既然锁都释放完毕了那么肯定是唤醒他们。doReleaseShared实现如下。
到此在读锁到源码已经说完了,接下来就是写锁的源码
写锁的lock源码
写锁的lock首先调用acquire方法
acquire方法只有拿到锁或者重入成功后才会返回ture,也就是只有没有拿到锁或者没有重入成功才会执行才会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法进行加入队列以及阻塞线程。
tryAcquire方法实现如下所示,可以看到只有重入成功或者加锁成功的时候才会返回true。
一旦加锁或者重入失败此时会先调用addWaiter方法加入等待队列中
加入队列后那么肯定是要阻塞线程,此时会调用acquireQueued方法,然后尝试再次获取锁,一旦失败会阻塞线程。
到此写锁的加锁源码结束。接下来是解锁过程。
写锁的unLock源码
到此写锁的加锁源码结束。接下来是解锁过程。解锁会先调用release方法,然后release方法会调用release方法。
通过tryRelease方法可以看到只有所有的写锁都释放完毕了才会进行唤醒,也就是调用exclusiceCount的时候等于0。一旦释放完毕此时release中会拿到头部节点然后唤醒队列头部的线程,此时你可能在想那写锁只是唤醒了头部获取读锁被阻塞的有多个,这些是怎么被唤醒的呢?实际上在读锁被阻塞中调用了doAcquireShared方法,一旦被释放,此时会将除了头部以外后续的节点都唤醒。