本章主要学习读写锁。
关于读写锁,在《 Java并发18》中已经学习过:
- synchronized关键字只提供了一种锁,即互斥锁。
- java.util.concurretn.locks包不仅通过Lock接口提供了与前者类似的互斥锁,而且还通过ReadWriteLock接口提供了读锁和写锁。
读写锁最大的优势在于读锁与读锁并不独占,提高了共享资源的使用效率。
本文有以下三部分内容:
- 学习ReadWriteLock接口的源码
- 学习ReetrantReadWriteLock类的源码
- 实例编码
1.ReadWriteLock接口
1.1.源码
/**
* A {@code ReadWriteLock} maintains a pair of associated {@link
* Lock locks}, one for read-only operations and one for writing.
* The {@link #readLock read lock} may be held simultaneously by
* multiple reader threads, so long as there are no writers. The
* {@link #writeLock write lock} is exclusive.
*
* <p>All {@code ReadWriteLock} implementations must guarantee that
* the memory synchronization effects of {@code writeLock} operations
* (as specified in the {@link Lock} interface) also hold with respect
* to the associated {@code readLock}. That is, a thread successfully
* acquiring the read lock will see all updates made upon previous
* release of the write lock.
*
* <p>A read-write lock allows for a greater level of concurrency in
* accessing shared data than that permitted by a mutual exclusion lock.
* It exploits the fact that while only a single thread at a time (a
* <em>writer</em> thread) can modify the shared data, in many cases any
* number of threads can concurrently read the data (hence <em>reader</em>
* threads).
* In theory, the increase in concurrency permitted by the use of a read-write
* lock will lead to performance improvements over the use of a mutual
* exclusion lock. In practice this increase in concurrency will only be fully
* realized on a multi-processor, and then only if the access patterns for
* the shared data are suitable.
*
* <p>Whether or not a read-write lock will improve performance over the use
* of a mutual exclusion lock depends on the frequency that the data is
* read compared to being modified, the duration of the read and write
* operations, and the contention for the data - that is, the number of
* threads that will try to read or write the data at the same time.
* For example, a collection that is initially populated with data and
* thereafter infrequently modified, while being frequently searched
* (such as a directory of some kind) is an ideal candidate for the use of
* a read-write lock. However, if updates become frequent then the data
* spends most of its time being exclusively locked and there is little, if any
* increase in concurrency. Further, if the read operations are too short
* the overhead of the read-write lock implementation (which is inherently
* more complex than a mutual exclusion lock) can dominate the execution
* cost, particularly as many read-write lock implementations still serialize
* all threads through a small section of code. Ultimately, only profiling
* and measurement will establish whether the use of a read-write lock is
* suitable for your application.
*
*
* <p>Although the basic operation of a read-write lock is straight-forward,
* there are many policy decisions that an implementation must make, which
* may affect the effectiveness of the read-write lock in a given application.
* Examples of these policies include:
* <ul>
* <li>Determining whether to grant the read lock or the write lock, when
* both readers and writers are waiting, at the time that a writer releases
* the write lock. Writer preference is common, as writes are expected to be
* short and infrequent. Reader preference is less common as it can lead to
* lengthy delays for a write if the readers are frequent and long-lived as
* expected. Fair, or "in-order" implementations are also possible.
*
* <li>Determining whether readers that request the read lock while a
* reader is active and a writer is waiting, are granted the read lock.
* Preference to the reader can delay the writer indefinitely, while
* preference to the writer can reduce the potential for concurrency.
*
* <li>Determining whether the locks are reentrant: can a thread with the
* write lock reacquire it? Can it acquire a read lock while holding the
* write lock? Is the read lock itself reentrant?
*
* <li>Can the write lock be downgraded to a read lock without allowing
* an intervening writer? Can a read lock be upgraded to a write lock,
* in preference to other waiting readers or writers?
*
* </ul>
* You should consider all of these things when evaluating the suitability
* of a given implementation for your application.
*
* @see ReentrantReadWriteLock
* @see Lock
* @see ReentrantLock
*
* @since 1.5
* @author Doug Lea
*/
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
1.2.源码翻译
上面的源码翻译如下:
一个ReadWriteLock锁包含一对关联的锁,一个锁用于只读操作,另一个锁用于写操作:
- 只要没有线程进行写操作,多个线程可以同时持有这个只读锁readLock。
- 写锁writeLock是独占的。
所有ReadWriteLock接口的实现类必须保证内存同步效果:所有写锁writeLock的相关操作都对只读锁readLock可见。
也就是说,如果一个线程成功的获取了只读锁readLock,那么这个线程可以看到上个写锁writeLock所做的所有修改。
相较于互斥锁,读写锁提供了更高级别的对共享数据的并发访问机制。
它响应了一个事实:虽然在同一时刻只有一个线程可以修改共享数据,但在很多情况下,多个线程可以并发的读取共享数据。
理论上,使用读写锁会比使用互斥锁将会在并发数据访问上有更好的性能体现。
实际上,这种并发性能的提高只能在多核处理器上,且只有在对共享数据的访问模式合适时是才能有所体现。
读写锁是否能够提高并发性能取决于读数据与写数据的比例、读操作和写操作的持续时间以及数据争用情况(即试图同时读取或写入数据的线程数量)。例如:
- 一个通过初始化进行数据填充的集合,在不经常修改的情况下被频繁搜索,这就是一个使用读数据的理想选择。
- 如果更新操作十分频繁,程序将花费大部分时间进行互斥锁定,那么对并发性能的提升将会很小。
最终,只有经过配置和测试,才能确定读写锁是否适合于您的应用程序。
尽管读写锁的操作很简单,但是为了保证它在程序中的有效性,这里有很多策略需要决断。这些策略的示例如下:
当一个写锁释放时,如果同时有读线程和写线程在等待锁,这时要将锁授予读线程还是和写线程?
- 选择将锁授予写锁是常见的:因为写操作耗时很短并且发生概率很低,。
- 选择将锁授予读锁是不常见的:因为如果当读操作很频繁并且耗时都很长,这将会导致写操作长时间的等待。
- 使用公平或者有序的实现方式,也是可能的。
当一个读线程整在活动,且一个写线程正在等待时,这时,是否要将锁授予其他的读线程?
- 如果选择将锁授予读线程,则可能会导致写线程无限期的等待。
- 如果选择将锁授予读线程,则会降低并发性能的潜力。
锁是否是可重入的?
- 一个持有过写锁的线程能够重新获取写锁?
- 一个持有写锁的线程能够同时获得读锁?
- 读锁本身是否可重入?
写锁和读锁是否可以相关转换?
- 一个写锁是否可以降级为一个不允许其他写线程插入的读锁?
- 一个读锁是否可以升级并优先于其他的读线程和写线程的写锁?
在评估一个给定的实现类是否适合您的程序时,您应该考虑以上的所有问题。
1.3.总结
对上面的翻译总结如下:
1.读锁与写锁
- 一个ReadWriteLock锁包含一对关联的锁:读锁和写锁
- 读锁readLock是共享的。
- 写锁writeLock是独占的。
2.理论上,读写锁比互斥锁有更好的性能体现的。
3.读写锁更适用于读多写少的情景。
4.实际上,读写锁是否能够带来性能的提升,是需要实际的测试与配置的。
2.ReetrantReadWriteLock类
2.1.源码
/**
* An implementation of {@link ReadWriteLock} supporting similar
* semantics to {@link ReentrantLock}.
* <p>This class has the following properties:
*
* <ul>
* <li><b>Acquisition order</b>
*
* <p>This class does not impose a reader or writer preference
* ordering for lock access. However, it does support an optional
* <em>fairness</em> policy.
*
* <dl>
* <dt><b><i>Non-fair mode (default)</i></b>
* <dd>When constructed as non-fair (the default), the order of entry
* to the read and write lock is unspecified, subject to reentrancy
* constraints. A nonfair lock that is continuously contended may
* indefinitely postpone one or more reader or writer threads, but
* will normally have higher throughput than a fair lock.
*
* <dt><b><i>Fair mode</i></b>
* <dd>When constructed as fair, threads contend for entry using an
* approximately arrival-order policy. When the currently held lock
* is released, either the longest-waiting single writer thread will
* be assigned the write lock, or if there is a group of reader threads
* waiting longer than all waiting writer threads, that group will be
* assigned the read lock.
*
* <p>A thread that tries to acquire a fair read lock (non-reentrantly)
* will block if either the write lock is held, or there is a waiting
* writer thread. The thread will not acquire the read lock until
* after the oldest currently waiting writer thread has acquired and
* released the write lock. Of course, if a waiting writer abandons
* its wait, leaving one or more reader threads as the longest waiters
* in the queue with the write lock free, then those readers will be
* assigned the read lock.
*
* <p>A thread that tries to acquire a fair write lock (non-reentrantly)
* will block unless both the read lock and write lock are free (which
* implies there are no waiting threads). (Note that the non-blocking
* {@link ReadLock#tryLock()} and {@link WriteLock#tryLock()} methods
* do not honor this fair setting and will immediately acquire the lock
* if it is possible, regardless of waiting threads.)
* <p>
* </dl>
*
* <li><b>Reentrancy</b>
*
* <p>This lock allows both readers and writers to reacquire read or
* write locks in the style of a {@link ReentrantLock}. Non-reentrant
* readers are not allowed until all write locks held by the writing
* thread have been released.
*
* <p>Additionally, a writer can acquire the read lock, but not
* vice-versa. Among other applications, reentrancy can be useful
* when write locks are held during calls or callbacks to methods that
* perform reads under read locks. If a reader tries to acquire the
* write lock it will never succeed.
*
* <li><b>Lock downgrading</b>
* <p>Reentrancy also allows downgrading from the write lock to a read lock,
* by acquiring the write lock, then the read lock and then releasing the
* write lock. However, upgrading from a read lock to the write lock is
* <b>not</b> possible.
*
* <li><b>Interruption of lock acquisition</b>
* <p>The read lock and write lock both support interruption during lock
* acquisition.
*
* <li><b>{@link Condition} support</b>
* <p>The write lock provides a {@link Condition} implementation that
* behaves in the same way, with respect to the write lock, as the
* {@link Condition} implementation provided by
* {@link ReentrantLock#newCondition} does for {@link ReentrantLock}.
* This {@link Condition} can, of course, only be used with the write lock.
*
* <p>The read lock does not support a {@link Condition} and
* {@code readLock().newCondition()} throws
* {@code UnsupportedOperationException}.
*
* <li><b>Instrumentation</b>
* <p>This class supports methods to determine whether locks
* are held or contended. These methods are designed for monitoring
* system state, not for synchronization control.
* </ul>
*
* <p>Serialization of this class behaves in the same way as built-in
* locks: a deserialized lock is in the unlocked state, regardless of
* its state when serialized.
*
* <p><b>Sample usages</b>. Here is a code sketch showing how to perform
* lock downgrading after updating a cache (exception handling is
* particularly tricky when handling multiple locks in a non-nested
* fashion):
*
* <pre> {@code
* class CachedData {
* Object data;
* volatile boolean cacheValid;
* final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
*
* void processCachedData() {
* rwl.readLock().lock();
* if (!cacheValid) {
* // Must release read lock before acquiring write lock
* rwl.readLock().unlock();
* rwl.writeLock().lock();
* try {
* // Recheck state because another thread might have
* // acquired write lock and changed state before we did.
* if (!cacheValid) {
* data = ...
* cacheValid = true;
* }
* // Downgrade by acquiring read lock before releasing write lock
* rwl.readLock().lock();
* } finally {
* rwl.writeLock().unlock(); // Unlock write, still hold read
* }
* }
*
* try {
* use(data);
* } finally {
* rwl.readLock().unlock();
* }
* }
* }}</pre>
*
* ReentrantReadWriteLocks can be used to improve concurrency in some
* uses of some kinds of Collections. This is typically worthwhile
* only when the collections are expected to be large, accessed by
* more reader threads than writer threads, and entail operations with
* overhead that outweighs synchronization overhead. For example, here
* is a class using a TreeMap that is expected to be large and
* concurrently accessed.
*
* <pre> {@code
* class RWDictionary {
* private final Map<String, Data> m = new TreeMap<String, Data>();
* private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
* private final Lock r = rwl.readLock();
* private final Lock w = rwl.writeLock();
*
* public Data get(String key) {
* r.lock();
* try { return m.get(key); }
* finally { r.unlock(); }
* }
* public String[] allKeys() {
* r.lock();
* try { return m.keySet().toArray(); }
* finally { r.unlock(); }
* }
* public Data put(String key, Data value) {
* w.lock();
* try { return m.put(key, value); }
* finally { w.unlock(); }
* }
* public void clear() {
* w.lock();
* try { m.clear(); }
* finally { w.unlock(); }
* }
* }}</pre>
*
* <h3>Implementation Notes</h3>
*
* <p>This lock supports a maximum of 65535 recursive write locks
* and 65535 read locks. Attempts to exceed these limits result in
* {@link Error} throws from locking methods.
*
* @since 1.5
* @author Doug Lea
*/
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {//...}
2.2.源码翻译
ReentrantReadWriteLock类是ReadWriteLock接口的实现,支持与之类似的语法。该类具有以下属性:
1.获取顺序
这个类不会强行指定访问锁的读写顺序,但是它支持一个可选的公平策略:
非公平模式(默认)
在非公平模式下,进入读锁和写锁的顺序取决于可重入约束,是不确定的。
在非公平模式下,连续争用可能会导致一个或者多个读线程或写线程进入无限等待状态。
不过,通常非公平锁会比公平锁有更高的吞吐量。
公平模式
在公平模式下,进入读锁和写锁的顺序使用一种近乎顺序的策略。
当当前持有的锁被释放时,等待时间最长的单个写线程会被授予写锁,或者如果有一组读线程比所有的写线程等待的时间都长,则这组读线程将被授予锁。
如果有写线程持有锁或者有写线程正在等待锁,试图去获取一个公平的读锁(不可重入)的读线程将被阻塞。
这个读线程不会获得锁,直到当前等待的所有写线程获取并释放了锁。
当然,如果写线程放弃了等待,使得等待队列中只剩下一个或者多个等待时间最长的读线程,并且当前读锁可用,则这些读线程将会被授予锁。
除非当前的读锁和写锁都是可用的,写线程尝试去获取一个公平的写锁(不可重入)才不会被阻塞。
2.可重入性
ReentrantReadWriteLock类定义的锁,允许读线程和写线程以 ReentrantLock的形式去获取读锁和写锁。
直到所有持有锁的写线程释放锁,不可重入的读线程才会被允许获取锁。
此外,写线程可以获取读锁,反过来,读线程不可以获取写锁。
3.可降级性
可重入性也允许写锁降级成为读锁:首先获取写锁,然后获取读锁,然后释放写锁。
但是,从读锁升级为写锁是不可能的。
4.可中断性
在尝试获取锁的过程中,读锁和写锁都可以被中断。
5.支持Condition
就像ReentrantLock一样,写锁支持Condition操作。
当然,这种Condition操作,只能被应用在写锁上。
读锁不支持Condition操作,readLock().newCondition()会抛出一个UnsupportedOperationException异常。
6.状态仪表盘
ReentrantReadWriteLock类提供了方法用于查看锁是被持有还是被争用。
这些方法是为监视系统状态而设计的,而不是用于同步控制。
这个类的序列化行为与内置锁的方式相同:当锁被序列化时,会无视其状态,一个序列化的锁必定是解锁状态。
示例用法:
下面的代码展示了如何在更新缓存之后,将写锁降级为读锁(当以非嵌套的形式处理多个锁时,异常处理特别棘手):
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();//获取读锁
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();//释放读锁
rwl.writeLock().lock();//获取写锁
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();//获取读锁
} finally {
rwl.writeLock().unlock(); // 释放写锁,但是继续持有读锁
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();//释放读锁
}
}
}
ReenTrantReadWriteLock锁可以被用于一些集合类型来提高并发性能。
当集合预计很大,而且访问的读线程明显多于写线程时,使用ReenTrantReadWriteLock锁是值得的。
例如,下面是一个类,这个类使用TreeMap,这个TreeMap预计会很大并且被并发访问:
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
实现注意事项:
此锁支持最多65535个递归写锁和65535个读锁。
超过这些限制的将导致锁定方法报错。
3.实例
实例场景:
本次需要编码的实例场景很简单,验证读锁和写锁的互斥关系:
- 写写互斥
- 写读互斥
- 读写互斥
- 读读共享
直接上代码:
/**
* <p>Lock接口-读写锁</p>
*
* @author hanchao 2018/3/18 15:40
**/
public class ReadWriteLockDemo {
//定义非公平的读写锁
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
/**
* <p>lock接口-读写锁</p>
*
* @author hanchao 2018/3/18 15:41
**/
public static void main(String[] args) throws InterruptedException {
/**
* 0 写写互斥
* 1 写读互斥
* 2 读写互斥
* 3 读读共享
*/
int type = 3;
switch (type) {
case 0://写写互斥
//共用同一个lock对象的写锁
Lock writeLock = lock.writeLock();
//写
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取写锁...");
writeLock.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了写锁.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
writeLock.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了写锁.");
}
}).start();
//写
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取写锁...");
writeLock.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了写锁.");
try {
Thread.sleep(1800);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
writeLock.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了写锁.");
}
}).start();
break;
case 1://写读互斥
//共用同一个lock对象的写锁、读锁
Lock writeLock1 = lock.writeLock();
Lock readLock1 = lock.readLock();
//写
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取写锁...");
writeLock1.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了写锁.");
try {
Thread.sleep(1900);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
writeLock1.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了写锁.");
}
}).start();
//读
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取读锁...");
readLock1.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了读锁.");
try {
Thread.sleep(2100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
readLock1.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了读锁.");
}
}).start();
break;
case 2://读写互斥
//共用同一个lock对象的写锁、读锁
Lock writeLock2 = lock.writeLock();
Lock readLock2 = lock.readLock();
//读
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取读锁...");
readLock2.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了读锁.");
try {
Thread.sleep(2200);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
readLock2.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了读锁.");
}
}).start();
//写
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取写锁...");
writeLock2.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了写锁.");
try {
Thread.sleep(1700);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
writeLock2.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了写锁.");
}
}).start();
break;
case 3://读读共享
//共用同一个lock对象的读锁
Lock readLock3 = lock.readLock();
//读
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取读锁...");
readLock3.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了读锁.");
try {
Thread.sleep(1800);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
readLock3.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了读锁.");
}
}).start();
//读
new Thread(() -> {
System.out.println("线程[" + Thread.currentThread().getName() + "]尝试获取读锁...");
readLock3.lock();
System.out.println("线程[" + Thread.currentThread().getName() + "]获取了读锁.");
try {
Thread.sleep(1600);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {//在finally代码块中是否锁
readLock3.unlock();
System.out.println("线程[" + Thread.currentThread().getName() + "]释放了读锁.");
}
}).start();
break;
default:
break;
}
Thread.sleep(500);
System.out.println("============================");
}
}
测试结果:
写写互斥:
线程[Thread-0]尝试获取写锁...
线程[Thread-0]获取了写锁.
线程[Thread-1]尝试获取写锁...
============================
线程[Thread-0]释放了写锁.
线程[Thread-1]获取了写锁.
线程[Thread-1]释放了写锁.
写读互斥:
线程[Thread-0]尝试获取写锁...
线程[Thread-1]尝试获取读锁...
线程[Thread-0]获取了写锁.
============================
线程[Thread-0]释放了写锁.
线程[Thread-1]获取了读锁.
线程[Thread-1]释放了读锁.
读写互斥:
线程[Thread-0]尝试获取读锁...
线程[Thread-1]尝试获取写锁...
线程[Thread-0]获取了读锁.
============================
线程[Thread-0]释放了读锁.
线程[Thread-1]获取了写锁.
线程[Thread-1]释放了写锁.
读读共享:
线程[Thread-0]尝试获取读锁...
线程[Thread-0]获取了读锁.
线程[Thread-1]尝试获取读锁...
线程[Thread-1]获取了读锁.
============================
线程[Thread-1]释放了读锁.
线程[Thread-0]释放了读锁.
4.总结
主要对ReentrantReadWriteLock类进行总结:
1.访问顺序
支持可选的公平策略,默认为非公平模式。
2.可重入性
- 与ReentrantLock类似。
- 写线程可以获取读锁,读线程不可以获取写锁。
3.可降级性
- 允许写锁降级成为读锁:首先获取写锁,然后获取读锁,然后释放写锁。
- 不允许从读锁升级为写锁。
4.可中断性
读锁和写锁都可以被中断。
5.支持Condition
只有写锁支持。
6.监测方法
ReentrantReadWriteLock类提供了一些方法用于查看锁的状态,而非同步控制。
7.互斥规则
- 写写互斥
- 写读互斥
- 读写互斥
- 读读共享