这是一个系列文章:
本篇主要的内容是:
- volatile关键字
- LockSupport类使用
- Lock类使用
volatile关键字
要动volatile关键字的使用,需要对java的内存模型&共享变量的可见性有一定的了解。在JVM中,共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存保存了该线程使用到的主内存的副本拷贝,线程对该变量的所有操作都必须在私有存中进行,而不能直接读写主内存中的变量。
在线程A中将共享变量修改为了某个值,这时候还没有同步到主内存中,然后线程B中保留了共享内存的旧值,就会导致共享变量值不一致,要解决这种方式最简单的就是加锁,但是使用synchronized或者是Lock等的方式会太重量了,比较合理的方式就是实用volatile。
volatile的特性:
- 保证可见性,不保证原子性。当些一个
volatile变量的时候,JVM会把线程本地内存也同步到主内存中,这个写操作会导致其他线程中的volatile变量缓存无效。 - 禁止指令重排
一个简单的例子,我们可以不使用synchronized那么重量级的来实现:
1 | static class JmmDemo implements Runnable { |
我们就可以不用synchronized去实现这样的一个功能,同时synchronized也无法避免指令的重排,它能做的是原子性,可见性和有序性。
参考文章:
LockSupport
LockSupport是java.util.concurrent.locks包下的一个类,可以用于暂停线程和恢复线程,它是使用Unsafe实现的,它的方法如下:
- park() : 暂停某个线程
- LockSupport.unpark(t1) :唤起某个线程
例子:
1 | // 打印的时间会>=2000 |
参考文章:LockSupport详解
Lock
Lock与synchronized是租用是比较类似的,但是它的使用范围会更广更优雅,Lock可以手动的释放锁,获取锁,可中断的获取锁,超时获取锁,相对而言会更加的灵活,同时它也可以用于替换wait/notify的那一套,避免可能出现的死锁(没有唤起wait()),比如多个生产者1个消费者的这种情况。
Lock是一个接口,定义如下
1 | public interface Lock { |
Lock的实现代表类是ReentrantLock(可重入锁),我们先看下ReentrantLock,它是依靠CAS和LockSupport实现的,先看下面一个简单的例子(用于替换synchronized):
1 | // lock()方法会阻塞住,直到能获取锁 |
只会打印100000,0或者是-100000,0,作用是与synchronized类似,需要注意的是我们都需要在finally中调用unlock()方法,避免程序运行出现了异常没有释放锁。
它的构造函数:
1 | // 非公平锁 |
非公平与公平锁的不同
- 非公平锁在调用
lock之后,首先会调用CAS进行一次抢夺,假如锁没有被占用,就直接获取锁返回 - 非公平锁在CSA失败后,和公平锁一样进入
tryAcquire,假如发现这时候锁被释放了,非公平锁就会直接CAS枪锁,但是公平锁会判断等待队列是由有线程处于等待状态,有就不会去抢夺,乖乖排在后面。 - 假如在
tryAcquire抢夺失败,则线程进入阻塞状态。
相对而言,非公平锁性能更好,当然非公平锁让获取锁的时间变得更加不确定,可能导致阻塞队列中的线程长期处于饥饿状态。文章来源:理解ReentrantLock的公平锁和非公平锁
使用了更多API的例子:
1 | // trylock() 例子,返回false的时候不阻塞,可以向下继续运行 |
synchronized与ReentrantLock的比较
synchronized是独占锁,加锁和解锁过程自动进行,易于操作。ReentrantLock也是独占锁,加锁和解锁需要手动,不易操作,但是非常灵活synchronized可重入,因为加锁和解锁自动运行,不必担心最后锁是否释放。ReentrantLock也可以重入,但是加锁和解锁需要手动进行,而且次数需要一样。synchronized不可响应中断,一个线程获取不到锁就只能一直等待,ReentrantLock可以响应中断。
ReentrantLock的lock方法调用需要再try块之外,假如是获取锁成功了的,则需要再finally中释放锁。
ReentrantLock还有一个Condition newCondition()方法,可以用于平替wait/notify。Condition是一个接口,它的定义是:
1 | public interface Condition { |
它大体与Object的wait/notify机制差不多,其中await()对应的wait(),signal()对应的notify()。Condition的await()与signal()都必须在lock()和unlock()之间才能使用。
我们使用wait/notify在设计生产者与消费者的时候,假如是一对多的情况下,则可能会有死锁或者是多活的情况,就要么就完全的阻塞住了,不能做到一个时刻只有一个线程执行。我们每一个ReentrantLock可以有多个Condition,它们可以共用一个锁,但是可以自己去调用await()/signal()而不影响别的Condition,但是wait/notify是只能同一个锁对象,都得调用这个锁的wait/notify。例如一对多的生产-消费者,例子如下:
1 | private static List<String> data = new ArrayList<>(); |
参考文章: