synchronized可以解决Java的并发问题,为什么jdk1.5之后还要推出Java并发包并提供多种锁呢?
1、synchronized与Lock的区别
虽然synchronized和Lock都能够实现同步功能,但是两者之间还是有一定区别的。
- synchronized隐式获取和释放锁,Lock需显示的获取和释放锁,具有更高的灵活性,但是如果不释放锁,容易造成死锁问题;
- synchronized如果获取不到锁,会阻塞线程,但是Lock提供非阻塞api,可以立即返回结果;
- synchronized是java提供的同步关键字,但是Lock接口下面的都是实现类,是Java类;
- synchronized不可中断,lock.lockInterruptibly()可以响应中断;
- synchronized是非公平锁,但是Lock可以提供公平及非公平两种;
2、Lock基本使用
Java中对锁也提供了不同的实现类,使用的最多的有ReentrantLock和ReentrantReadWriteLock,下面分别介绍重入锁和读写锁。
2.1、ReentrantLock
重入锁ReentrantLock,顾名思义,就是支持重新进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
使用ReentrantLock进行n次lock之后,同样需要n次unlock(),否则其他的线程就无法拿到锁,从而造成死锁。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
通过构造函数我们可以看到,直接传入boolean即可构造出公平锁和非公平锁。公平锁使用FIFO原则,在等待队列中等待时间最长的线程会最先抢到锁。但是公平锁的效率没有非公平锁的效率高,因为既然要保证严格的FIFO,那么就会出现频繁的线程上下文切换,在cpu进行现场上下文切换的时候,会耗费大量的cpu资源。对于非公平锁来说,允许同一个线程多次拿到锁,或者不是严格的遵循FIFO,所以线程的上下文切换相对于公平锁而言并不会那么频繁。
下面通过一个例子我们来了解下重入锁的基本使用:
package com.xiaohuihui.lock.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Desription: 可重入锁
* @Author: yangchenhui
*/
public class ReentrantLockDemo1 {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock(); // block until condition holds
try {
System.out.println("第一次获取锁");
System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
lock.lock();
System.out.println("第二次获取锁了");
System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
} finally {
lock.unlock();
lock.unlock();
}
System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
// 如果不释放,此时其他线程是拿不到锁的
new Thread(() -> {
System.out.println(Thread.currentThread() + " 期望抢到锁");
lock.lock();
System.out.println(Thread.currentThread() + " 线程拿到了锁");
}).start();
}
}
重入锁可以响应线程中断,下面通过一个代码示例来验证:
package com.xiaohuihui.lock.reentrantlock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Desription: 重入锁响应中断示例
* @Author: yangchenhui
*/
public class ReentrantLockDemo2 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo2 demo2 = new ReentrantLockDemo2();
Runnable runnable = () -> {
try {
demo2.test(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
// 等待0.5秒,让thread1先执行
Thread.sleep(500);
thread2.start();
// 两秒后,中断thread2
Thread.sleep(2000);
// 如果不使用lock.lockInterruptibly(),此时中断线程是,不会响应,获取到锁的线程会继续执行
thread2.interrupt();
}
public void test(Thread thread) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ", 想获取锁");
//注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
// lock.lock();
lock.lockInterruptibly();
try {
System.out.println(thread.getName() + "得到了锁");
// 抢到锁,10秒不释放
Thread.sleep(10000);
} finally {
System.out.println(Thread.currentThread().getName() + "执行finally");
lock.unlock();
System.out.println(thread.getName() + "释放了锁");
}
}
}
执行结果如下:
响应中断后,Thread-1后面的代码没有执行,如果使用lock.lock()或者使用同步关键之synchronized就不能够得到这种结果。
使用tryLock()如果能够获取到锁,会返回true,如果不能获取到锁,会立即返回false。
package com.xiaohuihui.lock.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Desription:
* @Author: yangchenhui
*/
public class ReentrantLockDemo3 {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
if (lock.tryLock()) {
System.out.println("当前线程为" + Thread.currentThread().getName() + "获取到锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
System.out.println("当前线程为" + Thread.currentThread().getName() + "释放锁");
};
Thread thread1 = new Thread(runnable);
thread1.start();
}
}
2.1、ReentrantReadWriteLock
synchronized和ReentrantLock都是属于排他锁,在同一时间内,只允许有一个线程获取到锁,但是ReentrantReadWriteLock是共享锁,在同一时间内,允许多个线程获取到读锁。
为了避免出现脏读的情况,ReentrantReadWriteLock提供锁降级机制:获取写锁 --> 获取读锁 --> 释放写锁。
读写锁是一对互斥锁,如果当前有线程持有读锁,那么所有线程(包含当前获取到唯一读锁的线程)不能够获取写锁。如果当前线程获取到写锁,可以再次获取写锁或者读锁。正是基于这样的机制,才保证了读写锁的锁降级机制能够有效的避免脏读。
下面使用一个示例了解读写锁的基本使用:
package com.xiaohuihui.lock.readwritelock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Desription: 读写锁示例
* @Author: yangchenhui
*/
public class ReentrantReadWriteLockDemo1 {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReentrantReadWriteLockDemo1 readWriteLock = new ReentrantReadWriteLockDemo1();
// 多线程同时读/写
new Thread(() -> {
readWriteLock.read(Thread.currentThread());
}).start();
new Thread(() -> {
readWriteLock.read(Thread.currentThread());
}).start();
new Thread(() -> {
readWriteLock.write(Thread.currentThread());
}).start();
}
/**
* 多线程读,共享锁
* @param thread
*/
public void read(Thread thread) {
readWriteLock.readLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行“读”操作");
}
System.out.println(thread.getName() + "“读”操作完毕");
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* 写
*/
public void write(Thread thread) {
readWriteLock.writeLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行“写”操作");
}
System.out.println(thread.getName() + "“写”操作完毕");
} finally {
readWriteLock.writeLock().unlock();
}
}
}
在AQS中,读锁主要操作state,可以通过加减来控制当前获取到读锁的线程数,但是写锁操作的是owner,如果state=0,并且owner=null,说明当前没有线程获取到读锁,并且也没有线程获取到写锁,使用CAS机制修改owner,如果修改成功,说明加锁成功。
3、AQS机制
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect, int update))来进行操作,能够保证状态的改变是安全的。
如果我们要设计一个同步框架,首先我们需要一个状态位用来表示当前锁的状态,其次参与锁争抢的线程,如果没有获取到锁,需要将其放到一个等待队列当中,同时为了保证可重入性,我们需要一个字段来存储当前获得锁的线程。遵循这个思路,我们去看看AQS中的类图。
在ReentrantLock当中,有Sync内部类来继承AQS,使用父类提供的方法线程修改同步器中的锁状态。
AbstractQueuedSynchronizer中使用Node节点来存储等待线程集合,从源码中可以看到是一个双向链表的数据结构。
AbstractOwnableSynchronizer中使用exclusiveOwnerThread存储独占锁的锁拥有者。
当我们调用lock.lock()时,会调用同步器中的compareAndSetState(int expect, int update)方法,使用unsafe操作state状态,如果操作成功,设置线程拥有者。
stateOffset在AbstractQueuedSynchronizer的静态代码块中进行初始化操作,state是一个volatile变量,保持线程可见性。
总体来说,AQS同步框架使用volatile + Unsafe的cas操作保持state的同步操作,同时使用LockSupport的park(),unpark()机制及Node链表来进行等待线程队列的添加和通知。
AQS提供模版方法和勾子函数,提供了一个扩展性很强的基础框架用于同步工具的构建,这个设计模式值得我们学习。
unlock()的源代码跟lock()差不多,大家有时间的可以跟一下。