Synchornized优化总结(二)

前面讲了 Java 系统是如何针对内部锁进行优化的。如果说内部锁的优化是 Java 系统自身完成的话,那么接下来的优化就需要通过代码实现了。

锁的开销主要是在争用锁上,当多线程对共享资源进行访问时,会出现线程等待。

即便是使用内存屏障,也会导致冲刷写缓冲器,清空无效化队列等开销。

为了降低这种开销,通常可以从几个方面入手,例如:减少线程申请锁的频率(减少临界区)和减少线程持有锁的时间长度(减小锁颗粒)以及多线程的设计模式。

减少临界区的范围

当共享资源需要被多线程访问时,会将共享资源或者代码段放到临界区中。

如果在代码书写中减少临界区的长度,就可以减少锁被持有的时间,从而降低锁被征用的概率,达到减少锁开销的目的。

如上图,尽量避免对一个方法进行加锁同步,可以只针对方法中的需要同步资源/变量进行同步。其他的代码段不放到 Synchronzied 中,减少临界区的范围。

减小锁的颗粒度

减小锁的颗粒度可以降低锁的申请频率,从而减小锁被争用的概率。其中一种常见的方法就是将一个颗粒度较粗的锁拆分成颗粒度较细的锁。

假设有一个类 ServerStatus,里面包含了四个方法:

  • addUser
  • addQuery
  • removeUser
  • removeQuery

如果分别在每个方法加上 Synchronized。在一个线程访问其中任意一个方法的时候,将锁住 ServerStatus,此时其他线程都无法访问另外三个方法,从而进入等待。

如果只针对每个方法内部操作的对象加锁,例如:addUser 和 removeUser 方法针对 users 对象加锁。又例如:addQuery 和 removeQuery 方法针对 queries 对象加锁。

假设,当一个线程池调用 addUser 方法的时候,只会锁住 user 对象。另外一个线程是可以执行 addQuery 和 removeQuery 方法的。

并不会因为锁住整个对象而进入等待。JDK 内置的 ConcurrentHashMap 与 SynchronizedMap 就使用了类似的设计。

读写锁

也叫做线程的读写模式(Read-Write Lock),其本质是一种多线程设计模式。

将读取操作和写入操作分开考虑,在执行读取操作之前,线程必须获取读取的锁。

在执行写操作之前,必须获取写锁。当线程执行读取操作时,共享资源的状态不会发生变化,其他的线程也可以读取。但是在读取时,不可以写入。

其实,读写模式就是将原来共享资源的锁,转化成为读和写两把锁,将其分两种情况考虑。

如果都是读操作可以支持多线程同时进行,只有在写时其他线程才会进入等待。

说完了读写锁的基本原理,再来看看参与的角色:

  • Reader(读者), 对 SharedResource 角色执行 Read 操作。
  • Writer(写者), 对 SharedResource 角色执行 Write 操作。
  • SharedResource(共享资源), 表示对 Reader 和 Writer 两者共享的资源。
  • ReadWriteLock(读写锁),

    提供了 SharedResource 角色实现 Read 操作和 Write 操作时所需的锁。

    针对 Read 操作提供 readLock 和 readUnlock,对 Write 操作提供 writeLock 和 writeUnlock。

特别需要注意的是,在这里需要解决读写冲突的问题。当线程 A 获取读锁时,如果有线程 B 正在执行写操作,线程 A 需要等待,否则会引起 read-write conflict(读写冲突)。

如果线程 B 正在执行读操作,线程 A 不需要等待,因为 read-read 不会引起 conflict(冲突)。

当线程 A 要获取写入锁时,线程 B 正在执行写操作,线程 A 需要等待,否则会引起 write-write conflict(写写冲突)。

如果线程 B 正在执行读操作,则线程 A 需要等待,否则会引起 read-write conflict(读写冲突)。

上面基本把读写锁的基本原理说完了,接下来通过一些代码片段来看看它是如何实现的。

我们通过 Data 类 SharedResource,ReaderThread 和 WriterThread 来实现 Reader 和 Writer,ReadWriteLock 类来实现读写锁。

首先来看 ReaderThread 和 WriterThread,它们的实现相对简单。仅仅调用 Data 类中的 Read 和 Write 方法来实现读写操作。

接下来就是 ReadWriteLock 类,它实现了读写锁的具体功能。其中的几个变量用来控制访问线程和写入优先级:

  • readingReaders: 正在读取共享资源的线程个数,整型。
  • waitingWriters: 正在等待写入共享资源的线程个数,整型。
  • writingWriters: 正在写入共享资源的线程个数,整型。
  • preferWriter: 写入优先级标示,布尔型,为 true 表示写入优先;为 false 表示读取优先。

里面包含了四个方法,分别是:

  • readLock
  • readUnlock
  • writeLock
  • writeUnlock

顾名思义,分别对应读锁定,读解锁,写锁定,写解锁的操作。两两组合以后一共四种方法。

在 ReadWriteLock 定义的四种方法中,各自完成不同的任务:

  • readLock,读锁。

    线程在读的时候,检查是否有写线程在执行,如果有就需要等待。同时还会观察,在写入优先的时候,是否有等待写入的线程。

    如果存在也需要等待,等待写入操作的线程完成再执行。如果以上条件都没有满足,那么进行读操作,并将读取线程数 +1。

  • readUnlock,读解锁。 线程在读操作完成以后,将读取线程数 -1。通知其他等待线程执行。
  • writeLock,写锁。 先将写等待线程数 +1。如果发现有正在读的线程或者有正写的线程,那么进入等待。否则,进行写操作,并将正在写操作线程数 +1。
  • writeUnlock ,写解锁。 线程在写操作完成以后,将写线程数 -1。通知其他等待线程执行。

最后,来看共享资源的类:Data。它主要承载读写的方法。需要注意的是在做读/写的前后,需要加上对应的锁。

例如:在做读操作(doRead)之前需要加上 readLock(读锁),在完成读操作以后释放读锁(readUnlock)。

又例如:在做写操作(doWrite)之前需要加上 writeLock(写锁),在完成写操作以后释放写锁(writeUnlock)。

上面的几个类已经介绍完了,如果需要测试可以通过调用 ReaderThread 和 WriterThread 来完成调试。