volatile、synchronized、lock详解
1、volatile
被volatile定义的变量被一个线程修改后,另一个线程可以感知到。
能够保证读的准确性,不能保证写的准确性
1.1 机理
volatile意味着可见性。先看一个例子:
static class MyRunnable implements Runnable {
private boolean isRun = true;
public void setRun(boolean run) {
isRun = run;
}
@Override
public void run() {
System.out.println("线程1 开始运行..." + DateUtils.INSTANCE.getCurrDataStr());
// 当 isRun 为true时,会一直循环。直至isRun为false
while (isRun) {
}
System.out.println("线程1 运行结束了..." + DateUtils.INSTANCE.getCurrDataStr());
}
}
再另启线程2去改变 isRun 的值,让线程1退出循环
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程2 开始运行..." + DateUtils.INSTANCE.getCurrDataStr());
try {
Thread.sleep(1000);
System.out.println("线程2 等待2s后,去停止运行线程1..." + DateUtils.INSTANCE.getCurrDataStr());
myRunnable.setRun(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
打印的结果可以看到,线程1并没有退出循环。因为线程2将 isRun 变量读取到它的内存空间进行修改后,写入主内存,但由于线程1一直在私有栈中读取 isRun 变量,没有在主内存中读取 isRun 变量,因此不会退出循环。
如果我们把isRun赋值行改为:
private volatile boolean isRun = true;
将其用 volatile 修饰,则强制该变量从主内存中读取。
这样我们也就明白了volatile的实现机理,即:
- 当一个线程要使用 volatile 变量时,它会直接从主内存中读取,而不使用自己工作内存中的副本。
- 当一个线程对一个 volatile 变量写时,它会将变量的值刷新到共享内存(主内存)中。
1.2 非原子性
原子性是指,对于一个操作,其操作的内容只有全部执行/全不执行两个状态,不存在中间态。而volatile不能锁定某组操作,防止其他线程的干扰,即没有其他线程的干扰,所以volatile是非原子性的。也就是非线程安全的。
所以如果想要使用一个原子性的修饰符来控制操作,即在操作变量时锁定变量,我们就需要另一个修饰词synchronized。
2、synchronized
synchronized作用的代码范围对于不同线程是互斥的,并且线程子啊释放锁的时候会将共享变量的值刷新到共享内存中。
对象设置:
修饰代码块时,需1个reference对象作为锁的对象
修饰实例方法时,默认的锁对象 = 当前对象
修饰类方法(静态)时,默认的锁对象 = 当前类的Class对象
2.1 synchronized与volatile的区别
1、使用:
volatile只可以修饰变量,synchronized可以修饰对象,类,方法,代码块,语句。
2、原子性:
volatile是非原子性的,只能保证读的准确性,不能保证写的准确性。多线程并发访问变量时,不会产生阻塞。synchronized是原子性的,只有获取的锁的线程才能进入临界区,从而保证了临界区的所有语句全部执行。多线程并发访问会产生阻塞。
3、机理:
当线程对volatile变量读时,会把工作内存中的值置为无效。当线程对synchronized变量读时,会在改线程锁定变量时将工作内存中的变量置为无效。
当线程对volalite变量写时,会把修改的值刷新到主内存。当线程对synchronized变量写时,会在线程释放锁的时候,将修改的值刷新到主内存。
2.2 注意点
1、无论synchronized加在方法上还是对象上,其修饰的都是对象,而不是方法或者某个代码块代码语句
2、每个对象只有一个锁与之相对联
3、实现同步需要很大的系统开销来做控制,不要做无谓的锁定
2.3 synchronized的作用域
synchronized的作用域只有两种。实际上,synchronized直接作用于内存中的一个内存块,因此,可以通过锁定内存块来锁定一个实例变量或者锁定一个静态区域。
1、某个对象实例内
synchronized aMethod(){} 可以防止多个线程同时访问这个对象的synchronized方法,如果对象有多个synchronized方法,则只要一个线程访问了任何一个synchronized方法,则其他线程都不能同时访问任何一个该对象的synchronized方法。(synchronized作用于对象,且每个对象只有一个锁)
2、某个类的范围
又或者说是作用于静态方法/静态代码块。synchronized static aMethod(){} 可以防止多个线程同时访问这个类中的synchronized static 方法,它可以对类的所有实例对象起作用。
2.4 synchronized应用
2.4.1 synchronized方法
每个实例对应一个lock,线程后的该含有synchronized方法的实例的锁才可以执行,否则一直阻塞。方法一旦执行,则一直到方法返回才可以释放锁。此后被阻塞的线程才能获得该锁。对于一个实例,其声明为synchronized的方法显然只有一个能处于执行状态。从而避免了类访问变量的冲突。
synchronized同步的开销很大,如果synchronized作用于一个比较大的方法上,显然是不合算的。
2.4.2 synchronized代码块
synchronized代码块形式如下:
synchronized (synchronizedObject){
//Some thing
}
代码块内部代码必须在获得synchronizedObject的锁时才能执行。需要重点说的是synchronized(this),这也是比较常用的代码块。
synchronized的效果类似于在方法前修饰,只是修饰的方位缩小成代码块。两个线程同时访问一个变量时,如果一个线程在执行synchronized的代码,那么该实例被锁定,另一个线程如果要访问该实例被synchronized作用的范围,则会被阻塞。
此外,如果不使用this作为锁,而是只是想让一段代码同步,可以临时创建如下锁:
private byte[] lock=new byte[0];
从操作码上讲,创建一个长度为0的数组对象是最经济的,只需要3条操作码。
2.4.3. synchronized静态方法
synchronized修饰静态方法时或者在普通方法中以类为对象如下形式:
class StaticSynchronized{
public void aMethod{
synchronized (StaticSynchronized.class){
//Some thing
}
}
}
为synchronized静态方法。
注意的是,对于同一个类,其static和实例方法如果都用synchronized修饰,其作用的必然不是同一个对象。
2.4.4. synchronized对象
比较简单粗暴的实现形式,直接把对象锁定,思路也很清晰。java负责跟踪被加锁的对象,该锁定对象的线程每次给对象加锁时对象的计数器+1,每次解锁时计数器-1,如果对象的计数器为0,那么解除该线程的锁定。
2.5 synchronized的缺陷
synchronized修饰的代码只有获取到锁的线程才能够执行,其他线程只能等待该线程释放锁。一个线程释放锁的情况有以下方法:
- 获取锁的线程完成了synchronized修饰的代码块的执行;
- 线程执行时发生异常,JVM自动释放锁。
锁会因为等待I/O,sleep()方法等原因被阻塞而不释放锁,此时如果线程还处于用synchronized修饰的代码块区域里,那么其他线程只能等待,这样就影响了效率。因此java提供了Lock来实现另一个机制,即不让线程无限期的等待下去。
思考一个场景,当多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,读和读操作不会发生冲突。如果使用synchronized来修饰读和写的话,就很可能操作多个读操作无法同时进行的可能。如果只有synchronized修饰写的话,又有可能造成读写冲突。此时就需要用到Lock。
3、Lock
Lock不是语言内置的,synchronized是java关键字,为内置特性。Lock是一个类,通过这个类可以实现同步访问。
使用synchronized不需要我们手动的去控制加锁和缩放,系统会自动控制。而使用Lock类需要手动的加锁和释放。不主动释放可能会造成死锁。
3.1 Lock类接口设计
public interface Lock {
// 获取锁。
// 如果锁不可用,则当前线程将被禁用以用于线程调度目的并处于休眠状态,直到获得锁为止。
void lock();
// 与lock用法一样。
// 与lock的区别是:
// lockInterruptibly锁定的线程处于等待状态时,允许其他线程的打断。比如说获取到锁的线程A可以调用线程B.interrupt(),来中断等待中的线程B,并抛出一个interruptException;
// lock锁定的线程如果在等待时检测到线程使用interrupt,则会继续尝试获取锁,失败则继续失眠,只是在成功获取到锁之后再把当前线程置为interrupt状态。
void lockInterruptibly() throws InterruptedException;
// 如果锁可用,则获取锁并立即返回值为true 。如果锁不可用,则此方法将立即返回值false 。
boolean tryLock();
// 设定了一个等待时间,如果在这个时间内获取到了锁,则返回true,否色返回false结束
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁。一般放在异常处理操作的finally字符控制的代码块中。
void unlock();
// 返回绑定到此Lock实例的新Condition实例。
// 在等待条件之前,锁必须由当前线程持有。调用Condition.await()将在等待之前自动释放锁,并在等待返回之前重新获取锁。
Condition newCondition();
}
3.2 ReentrantLock可重入锁
ReentrantLock是一个类,实现了Lock接口。
3.2.1 可重入定义
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称之为可重入。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子线程仍然是安全的。
3.2.2 可重入的条件
- 不在函数内使用静态或全局数据
- 不返回静态或全局数据,所有数据都由函数的调用者提供
- 使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据
- 不调用不可重入函数
3.2.3 可重入与线程安全
可重入一般都是线程安全的。但线程安全的不一定都是可重入的。在不加锁的前提下,如果一个函数用到了全局或者静态变量,那么它不是线程安全的,也不是可重入的。如果我们加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的加锁方式是针对不同线程的访问,当同一个线程多次访问就会产生问题。只有当函数满足可重入的四条件时,才是可重入的。
3.2.4 synchronized是可重入锁
如果一个获取到锁1的线程A,请求一个被线程B持有的锁2时,则线程A会进入到阻塞状态。如果线程A还是请求锁1,锁1是可重入锁,请求就会成功。
synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此在一个线程使用synchronized方法时,调用该对象的另一个synchronized方法,即一个线程的得到一个对象锁之后再次请求该对象锁是永远可以拿到的。
3.2.5 synchronized可重入锁的实现
前面提到过,每个锁关联了一个线程持有者和一个计数器。当计数器为0时,说明没有被任何线程所持有,那么任何线程都可能获得该锁而调用相应的方法。当一个线程请求锁成功后,jvm就会记录下持有锁的线程,并将计数器+1。此时其他线程请求该锁,则必须等待。而是已经持有该锁的线程再次请求该所,则可以再次拿到这个锁,同时计数器会继续+1。当线程每退出一个synchronized方法/代码块时,计时器就会-1,直至为0,释放该锁。
3.2.6 ReentrantLock原理
ReentrantLock主要是利用CAS和AQS队列来实现的。支持公平锁和非公平锁,两者实现类似。
CAS:
compare and swap。比较并交换。CAS有3个操作数:内存值V,预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,才将内存值V修改为B,否则什么都不做。无论是哪种情况,它都会在CAS指令前返回该位置的值。该操作是一个原子操作,被广泛的应用在java的底层实现中。在java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
AQS:
AbstractQueuedSynchronizer。是一个用于构建锁和同步容器的框架。事实上java.util.concurrent包内许多类都是基于AQS构建,例如ReentrantLock、CountDownLatch、FurureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS是一个FIFO队列,代表排队等待锁的线程。队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他节点与等待线程关联,每个节点维护一个等待状态waitStatus。
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果同时还有另一个线程来尝试获取锁,当这个锁是非公平锁时,则有可能被这个线程抢先获取到。但如果锁是公平锁,它就会发现自己不是在队首的话,就会排到队尾,由队首的线程去获取锁。
ReentrantLock提供了两个构造器,默认是非公平锁。由lock()和unlock的源码可以看到,它 们只是分别调用了sync对象的lock()和release(1)方法。
NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先用一个CAS操作,判断state是否为0(表示当前锁未被占用),如果是0,则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的就去乖乖排队了。
"非公平"即提现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待的线程还未被唤醒,新来的线程直接抢了该锁,那么就相当于插队了。
若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到else中。
NonfairSync.lock()
--> sync.lock();
// 调用的是NonfairSync.lock()
--> final void lock() { @NonfairSync
// CAS操作,判断state是否为0(表示当前锁未被占用),如果是0,则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败
acquire(1);
--> public final void acquire(int arg) { @AbstractQueuedSynchronizer
// 再次尝试去获取锁。细节看下面①。。
if (!tryAcquire(arg)
// 如果上面尝试获取锁失败,addWaiter 是入队操作。流程看下面②。。。。。。。。
// acquireQueued 是挂起操作。流程看下面③。。。。。。。。
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
①:尝试去获取锁。如果尝试获取成功,方法直接返回。
非公平锁tryAcquire的流程是:检查state状态,若为0,表示锁未被占用,那么尝试占用。若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果都没有成功,则获取锁失败,返回false。
protected final boolean tryAcquire(int acquires) { @NonfairSync
// 尝试非公平锁,调用的是 Sync.nonfairTryAcquire()
return nonfairTryAcquire(acquires);
--> final boolean nonfairTryAcquire(int acquires) { @Sync
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state值
int c = getState();
// state == 0 ,表示没有线程占用锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 占用锁成功,设置独占锁线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经占用该锁了。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state值为新的重入次数
setState(nextc);
return true;
}
return false;
}
}
②:尝试获取锁失败,那就要将当前线程入队。
// 将新节点和当前线程关联,并入队
private Node addWaiter(Node mode) {
// 初始化节点,设置关联线程和模式(独占 or 共享)
Node node = new Node(mode);
// for循环,直到入队成功返回
for (;;) {
// 获取尾节点引用
Node oldTail = tail;
// 尾节点不为空,说明队列已经初始化过了
if (oldTail != null) {
U.putObject(node, Node.PREV, oldTail);
// 设置新节点为尾节点
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
// 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
initializeSyncQueue();
// 在第一次争用时初始化头部和尾部字段
--> private final void initializeSyncQueue() {
Node h;
if (U.compareAndSwapObject(this, HEAD, null, (h = new Node())))
tail = h;
}
}
}
}
这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于initializeSyncQueue的实现使用CAS操作,所以只有一个线程会创建head节点成功。
③:入队后进行挂起。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起
// 已经入队的线程尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
try {
// 标记线程是否被中断过
boolean interrupted = false;
for (;;) {
// 获取当前节点的前面一个节点
final Node p = node.predecessor();
// 如果前面一个节点是是head,则该节点排第二。便有资格去尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取成功,将当前节点设置为head节点
setHead(node);
// 原head节点出队,在某个时间点被gc回收
p.next = null; // help GC
// 返回是否被中断过
return interrupted;
}
// 判断获取失败后是否可以挂起,若可以则挂起。接着看下面的④。。。。。。。。。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
④:判断当前线程获取锁失败之后是否需要挂起
// 检查并更新未能获取的节点的状态。如果线程应该阻塞,则返回 true。这是所有采集循环中的主要信号控制。要求 pred == node.prev。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前面一个节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前面一个节点的状态为signal,返回true
return true;
// 前面一个节点的状态为cancelled,返回true
if (ws > 0) {
// 从队尾向前寻找第一个状态不为cancelled的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为signal
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
// 挂起当前线程,返回线程中断状态并重置
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程入队后能过挂起的前提是,它的前驱节点的状态是signal,它的含义是,提醒前面的节点,如果前面的节点获取到锁并出队后,把自己唤醒。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,如符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否大于0(canceled),若是那么向前遍历直到找到第一个符合条件的前驱,若不是则将前驱节点的状态设置为singal。
NonfairSync.unlock()
// 尝试释放此锁。如果当前线程是这个锁的持有者,那么持有计数就会递减。如果保持计数现在为零,则释放锁。
public void unlock() {
sync.release(1);
--> public final boolean release(int arg) { @AbstractQueuedSynchronizer
// 调用 Sync.tryRelease()
// 尝试释放锁,如果释放成功,那么查看头节点的状态是否是signal,如果是则唤醒头节点的下一个节点关联的线程,如果释放失败,那么返回false。表示解锁失败。
// 具体细节,看下面
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
尝试释放当前线程占用的锁
protected final boolean tryRelease(int releases) {
// 计算释放后state值
int c = getState() - releases;
// 如果不是当前线程占用锁,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 锁被重入次数为0,表示释放成功
free = true;
// 清空独占线程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
入参是1。tryRelease的过程是:当前释放锁的线程若不持有锁,那就抛出异常。若持有锁,计算释放后的state是否为0,若为0则表示锁已经被成功释放,并且清空独占线程。最后更新state值,返回free。
FairSync
公平锁和非公平锁的不同之处在于,公平锁在获取锁的时候,不会先去检查state的状态,而是直接执行
final void lock() {
acquire(1);
}
超时机制
在ReentrantLock的tryLock(long timeout, TimeUnit unit)提供了超时获取锁的功能。如果在指定时间内获取到了锁就返回true,如果没有就返回false。这种机制避免了线程无限期等待锁的释放。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
还是看一下具体实现
public boolean tryLock(long timeout, TimeUnit unit) @ReentrantLock
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
--> public final boolean tryAcquireNanos(int arg, long nanosTimeout) @AbstractQueuedSynchronizer
throws InterruptedException {
// 如果线程被中断了,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 先尝试获取锁,获取成功就直接返回。获取失败则进入doAcquireNanos。下面看下doAcquireNanos()
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
}
在有限的时间内去竞争锁
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待时间小于0,直接返回获取锁失败
if (nanosTimeout <= 0L)
return false;
// 计算最后尝试获取锁的时间点
final long deadline = System.nanoTime() + nanosTimeout;
// 线程入队
final Node node = addWaiter(Node.EXCLUSIVE);
try {
// 又是自旋
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点,并且获取锁成功,则将当前节点变成头节点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
// 计算尝试获取锁的剩余时长。
nanosTimeout = deadline - System.nanoTime();
// 如果已经超时,则取消获取。返回false
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 超时时间未到,且需要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
// 阻塞当前线程知道超时时间到期
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
doAcquireNanos的流程简述为:线程先入队等待,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列中找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是signal,那么在当前这一轮循环中线程不会被挂起,然后更 新超时时间,开始新一轮的尝试。
3.3 ReadWriteLock读写锁
3.3.1 ReadWriteLock接口
public interface ReadWriteLock {
// 返回读锁。
Lock readLock();
// 返回写锁
Lock writeLock();
}
提供了读和写两个操作,两个锁的存在,是为了将读和写分开来操作。为了提高效率,多个线程可以同时进行读的操作,但写锁是独占的。而读和写又不能同时进行。
3.3.2 ReentrantReadWriteLock可重入读写锁
ReentrantReadWriteLock是ReadWriteLock接口的唯一实例。同时提供了很多操作方法。
读写锁代码示例:
// 读
class StartReadRunnable implements Runnable {
@Override
public void run() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":开始读。。。。。" + DateUtils.INSTANCE.getCurrDataStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":结束读。。。。。" + DateUtils.INSTANCE.getCurrDataStr());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}
// 写
static class StartWriteRunnable implements Runnable {
@Override
public void run() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":开始写。。。。。" + DateUtils.INSTANCE.getCurrDataStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":结束写。。。。。" + DateUtils.INSTANCE.getCurrDataStr());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
class ReadAndWriteLockDemo {
static ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
static Lock readLock = mReadWriteLock.readLock();
static Lock writeLock = mReadWriteLock.writeLock();
public static void main(String[] args) {
// 创建8个线程,2个线程写,6个线程读。
ExecutorService executorService = Executors.newFixedThreadPool(8);
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
executorService.execute(new StartReadRunnable());
} else {
executorService.execute(new StartWriteRunnable());
}
}
for (int i = 0; i < 4; i++) {
executorService.execute(new StartReadRunnable());
}
}
打印日志:
pool-1-thread-1:开始读。。。。。22/04/08 14:15:30
pool-1-thread-1:结束读。。。。。22/04/08 14:15:32
pool-1-thread-2:开始写。。。。。22/04/08 14:15:32 // 线程1读完,线程2才能接着写
pool-1-thread-2:结束写。。。。。22/04/08 14:15:34
pool-1-thread-4:开始写。。。。。22/04/08 14:15:34 // 线程2写完,线程4才能接着写
pool-1-thread-4:结束写。。。。。22/04/08 14:15:36
pool-1-thread-3:开始读。。。。。22/04/08 14:15:36 // 线程4写完,线程3才能接着写
pool-1-thread-5:开始读。。。。。22/04/08 14:15:36
pool-1-thread-6:开始读。。。。。22/04/08 14:15:36
pool-1-thread-8:开始读。。。。。22/04/08 14:15:36
pool-1-thread-7:开始读。。。。。22/04/08 14:15:36
pool-1-thread-3:结束读。。。。。22/04/08 14:15:38
pool-1-thread-5:结束读。。。。。22/04/08 14:15:38
pool-1-thread-8:结束读。。。。。22/04/08 14:15:38
pool-1-thread-6:结束读。。。。。22/04/08 14:15:38
pool-1-thread-7:结束读。。。。。22/04/08 14:15:38 // 线程3、4、5、6、7、8可以同时读
3.4 公平锁
公平锁即当多个线程等待同一个锁的时候,当锁被释放了,并不是多个线程随机获取到锁,而是被等待时间最长的线程获取到。synchronized是一个非公平锁,无法保证获取到锁的先后顺序。ReentrantLock和ReentrantReadWriteLock默认也是非公平锁,但可以在构造方法中传入一个boolean值,来标识是否是公平的。
// 创建ReentrantLock的实例。这相当于使用ReentrantLock(false)
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁
}
// 使用给定的公平策略创建ReentrantLock的实例。
// 参数:fair – 如果此锁应使用公平排序策略,则为true
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
参考上面的读写实例,可以看到线程并不是按顺序执行的。如果我们在ReentrantReadWriteLock构造方法中传入true,则打印结果如下:
pool-1-thread-1:开始读。。。。。22/04/08 14:28:16
pool-1-thread-1:结束读。。。。。22/04/08 14:28:18
pool-1-thread-2:开始写。。。。。22/04/08 14:28:18
pool-1-thread-2:结束写。。。。。22/04/08 14:28:20
pool-1-thread-3:开始读。。。。。22/04/08 14:28:20
pool-1-thread-3:结束读。。。。。22/04/08 14:28:22
pool-1-thread-4:开始写。。。。。22/04/08 14:28:22
pool-1-thread-4:结束写。。。。。22/04/08 14:28:24
pool-1-thread-5:开始读。。。。。22/04/08 14:28:24
pool-1-thread-6:开始读。。。。。22/04/08 14:28:24
pool-1-thread-7:开始读。。。。。22/04/08 14:28:24
pool-1-thread-8:开始读。。。。。22/04/08 14:28:24
pool-1-thread-6:结束读。。。。。22/04/08 14:28:26
pool-1-thread-7:结束读。。。。。22/04/08 14:28:26
pool-1-thread-5:结束读。。。。。22/04/08 14:28:26
pool-1-thread-8:结束读。。。。。22/04/08 14:28:26
3.5 Lock和synchronized的选择
- synchronized是内置语言实现的关键字,Lock是为了实现更高级锁功能而提供的接口;
- Lock实现了tryLock等接口,线程未获取到锁时不用一直等待;
- synchronized发生异常时自动释放占有的锁,Lock需要在finally块中手动释放锁。
- Lock可以通过Lock.lockInterruptibly()来中断锁;
- 由于Lock提供了时间限制同步,可被打断同步等机制,线程激烈竞争时Lock的性能远优于synchronized,即有大量线程时推荐使用Lock
- ReentrantReadWriteLock实现了封装好的读写锁用于大量读少量写的场景。解决了synchronized难以读写同步的问题。