2019独角兽企业重金招聘Python工程师标准>>>
标准 JDK中提供了多种显示锁的实现,将在下面的章节中进行介绍。
一.ReentrantLock
ReentrantLock 类是一个互斥锁,它和 synchronized 关键字访问的隐式锁具有相同的功能,但它具有扩展功能。它也实现了可重入的功能。
如何使用 ReentrantLock
锁通过 lock() 获取,通过 unlock() 释放,将代码封装到 try/finally 块中是非常重要的,以确保在出现异常的时候也能释放锁。这个方法和使用关键字 synchronized 修饰的方法是一样是线程安全的。如果一个线程已经获得了锁,后续线程调用 lock() 会暂停线程,直到锁被释放,永远只有一个线程能获取锁。
lock 支持更细粒度的去控制一个方法的同步,如下面的代码:
当第一个任务获取锁时,第二个任务获取锁的状态信息:
作为 lock() 方法的替代方法 tryLock() 尝试去获取锁而不暂停当前线程,必须使用 boolean 结果去判断是否真的获取到了锁。
二.ReadWriteLock
读写锁意思就是:读读共享,读写互斥,写写互斥。意思是如果两个方法都是调用读锁,那么多线程可以并发访问。但是一个方法调用读方法,一个调用写方法,那么该锁就会变成同步锁(一个方法完了才去执行另一个)。
ReadWriteLock 指定了另一种类型的锁,即读写锁。读写锁实现的逻辑是,当没有线程在写这个变量时,其他的线程可以读取这个变量,所以就是当没有线程持有写锁时,读锁就可以被所有的线程持有。如果读取比写更频繁,这将增加系统的性能和吞吐量。
上面的例子首先获取一个写入锁,在 sleep 1秒后在 map 中写入值,在这个任务完成之前,还有两个任务正在提交,试图从 map 读取值:
当执行上面的代码时,你会注意到两人读取的任务必须等待直到写入完成(当在读取的时候,写是不能获取锁的)。写入锁释放后,两个任务并行执行,它们不必等待对方是否完成,因为只要没有线程持有写入锁,它们就可以同时持有读取锁。
三.StampedLock
Java 8 提供了一种新类型的锁 StampedLock,像上面的例子一样它也支持读写锁,与 ReadWriteLock 不同的是,StampedLock 的锁定方法返回一个 long 值,可以利用这个值检查是否释放锁和锁仍然有效。另外 StampedLock 支持另外一种称为乐观锁的模式。
下面使用 StampedLock 来替换 ReadWriteLock
通过 readLock() 和 writeLock() 方法来获取读写锁会返回一个稍后用于在 finally 块中释放锁的值。注意,这里的锁不是可重入的。每次锁定都会返回一个新的值,并在没有锁的情况下阻塞,在使用的时候要注意不要死锁。
就像前面 ReadWriteLock 中的示例一样,两个读取任务必须等待写入任务释放锁。然后同时并行执行打印结果到控制台。
下面的例子演示了乐观锁
通过调用 tryOptimisticRead() 来获取乐观读写锁,tryOptimisticRead()总是返回一个值,而不会阻塞当前线程,也不关锁是否可用。如果有一个写锁激活则返回0。可以通过 lock.validate(stamp) 来检查返回的标记(long 值)是否有效。
执行上面的代码输出:
因此,在使用乐观锁时,必须在每次访问任何共享的变量后验证锁,以确保读取仍然有效
有时将读锁转换为写锁并不需要再次解锁和锁定是有用的。StampedLock 为此提供了tryConvertToWriteLock() 方法,如下面的示例所示:
该任务首先获得一个读锁,并将当前的变量计数值打印到控制台。 但是,如果当前值为 0,我们要分配一个新的值23。我们首先必须将读锁转换为写锁,以不打破其他线程的潜在并发访问。 调用 tryConvertToWriteLock() 不会阻塞,但可能会返回 0,指示当前没有写锁定可用。 在这种情况下,我们调用writeLock()来阻塞当前线程,直到写锁可用。
四.Semaphores
除了锁之外,并发API还支持计数信号量。 锁通常授予对变量或资源的独占访问权,而信号量则能够维护整套许可证。 在不同的情况下,必须限制对应用程序某些部分的并发访问量。
下面是一个如何限制对长时间任务的访问的例子:
执行程序可以同时运行10个任务,但是我们使用5信号量,因此限制并发访问为5个。使用try/finally块,即使在异常的情况下正确释放信号量也是非常重要的。
当有 5 个任务获取型号量后,随后的任务便不能获取信号量了。但是如果前面 5 的任务执行完成,finally 块释放了型号量,随后的线程就可以获取星号量了,总数不会超过5个。这里调用 tryAcquire() 获取型号量设置了超时时间1秒,意味着当线程获取信号量失败后可以阻塞等待1秒再获取。