为什么要Lock:synchronized的缺陷
(1)假设两个线程竞争同一个锁,如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这将影响程序执行效率。
(2)因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
(3)再举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,同样通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
(4)Lock与synchronized的区别:
- synchronized是java语言内置的,Lock是一个类,通过这个类可以实现同步访问。
- synchronized不用手动释放锁,Lock需要手动释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
(5)synchronized:
- 优点:实现简单,语义清晰,便于JVM堆栈跟踪;加锁解锁过程由JVM自动控制,提供了多种优化方案。
- 缺点:不能进行高级功能(定时,轮询和可中断等)。
(6)Lock:
- 优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁
- 缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪。
(7)使用哪个:
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized。
接口Lock
Lock是一个接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
使用最多,获取锁,如果锁已被其他线程获取,则进行等待。
假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法不会中断线程B的等待过程。
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
lockInterruptibly()
当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
public void method() throws InterruptedException {
Lock lock = ...;
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
tryLock()
尝试获取锁,如果获取成功,则返回true,如果获取失败,则返回false。
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
tryLock(long time, TimeUnit unit)
拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
实现类ReentrantLock
ReentrantLock 是“可重入锁”,ReentrantLock 是唯一实现了Lock接口的类,并且 ReentrantLock 提供了更多的方法。
public class ReentrantLock implements Lock, java.io.Serializable {}
使用lock()
public class Main {
public static void main(String[] args) {
MyConcurrentList myConcurrentList = new MyConcurrentList();
Thread t1 = new Thread(() -> myConcurrentList.insert("Thread-1"));
t1.start();
Thread t2 = new Thread(() -> myConcurrentList.insert("Thread-2"));
t2.start();
}
}
class MyConcurrentList {
private Lock lock = new ReentrantLock();
private ArrayList<String> arrayList = new ArrayList<>();
public void insert(String name) {
lock.lock();
try {
System.out.println(name + " get lock");
arrayList.add(name);
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(name + " release lock");
lock.unlock();
}
}
}
输出:
Thread-1 get lock
Thread-1 release lock
Thread-2 get lock
Thread-2 release lock
使用tryLock()
public class Main {
public static void main(String[] args) {
MyConcurrentList myConcurrentList = new MyConcurrentList();
Thread t1 = new Thread(() -> myConcurrentList.insert("Thread-1"));
t1.start();
Thread t2 = new Thread(() -> myConcurrentList.insert("Thread-2"));
t2.start();
}
}
class MyConcurrentList {
private Lock lock = new ReentrantLock();
private ArrayList<String> arrayList = new ArrayList<>();
public void insert(String name) {
if (lock.tryLock()) {
try {
System.out.println(name + " get lock success");
arrayList.add(name);
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(name + " release lock");
lock.unlock();
}
} else {
System.out.println(name + " get lock fail");
}
}
}
结果1:两个线程依次获得锁
Thread-1 get lock success
Thread-1 release lock
Thread-2 get lock success
Thread-2 release lock
结果2:Thread-2获取锁失败后,Thread-1才准备释放锁
Thread-1 get lock success
Thread-2 get lock fail
Thread-1 release lock
结果3:Thread-1将要释放锁了,Thread-2已经获取锁失败
Thread-1 get lock success
Thread-1 release lock
Thread-2 get lock fail
使用lockInterruptibly()
public class Main {
public static void main(String[] args) {
MyConcurrentList myConcurrentList = new MyConcurrentList();
Thread t1 = new MyThread(myConcurrentList);
t1.start();
Thread t2 = new MyThread(myConcurrentList);
t2.start();
//暂停一段时间,确保t2在等待锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t2,让它抛出InterruptedException异常
t2.interrupt();
}
}
class MyThread extends Thread {
private MyConcurrentList myConcurrentList = null;
public MyThread(MyConcurrentList myConcurrentList) {
this.myConcurrentList = myConcurrentList;
}
@Override
public void run() {
//调用并发安全的方法
try {
myConcurrentList.insert(this.getName());
} catch (InterruptedException e) {
System.out.println(this.getName() + " 被中断");
}
}
}
/*并发安全的List*/
class MyConcurrentList {
private Lock lock = new ReentrantLock();
private ArrayList<String> arrayList = new ArrayList<>();
public void insert(String name) throws InterruptedException {
//注意,如果需要正确中断等待锁的线程,必须将获取锁放在try-catch外面,然后将InterruptedException抛出
//如果当前线程在等待锁时,当前线程被中断,则会抛出InterruptedException异常
lock.lockInterruptibly();
try {
System.out.println(name + " 得到锁");
//模拟耗时操作2s
long startTime = System.currentTimeMillis();
while (true) {
if (System.currentTimeMillis() - startTime >= 2000) {
break;
}
}
} finally {
System.out.println(name + " 执行finally");
lock.unlock();
System.out.println(name + " 释放了锁");
}
}
}
输出:
Thread-0 得到锁
Thread-1 被中断
Thread-0 执行finally
Thread-0 释放了锁
接口ReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
// Returns the lock used for reading.
Lock readLock();
// Returns the lock used for writing.
Lock writeLock();
}
实现类ReentrantReadWriteLock
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:
- readLock() 用来获取读锁
- writeLock() 用来获取写锁
使用readLock()
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
ConcurrentFileUtil concurrentFileUtil = new ConcurrentFileUtil();
Thread t1 = new Thread(() -> concurrentFileUtil.read("Thread-0"));
t1.start();
Thread t2 = new Thread(() -> concurrentFileUtil.read("Thread-1"));
t2.start();
}
}
class ConcurrentFileUtil {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read(String name) {
//先获取锁
readWriteLock.readLock().lock();
//拿到锁后
try {
System.out.println(name + " 开始读");
Thread.sleep(1000);//模拟耗时操作
System.out.println(name + " 结束读");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
System.out.println(name + " 释放了锁");
}
}
}
结果:
Thread-0 开始读
Thread-1 开始读
Thread-0 结束读
Thread-0 释放了锁
Thread-1 结束读
Thread-1 释放了锁
使用writeLock()
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
ConcurrentFileUtil concurrentFileUtil = new ConcurrentFileUtil();
Thread t1 = new Thread(() -> concurrentFileUtil.write("Thread-0"));
t1.start();
Thread t2 = new Thread(() -> concurrentFileUtil.write("Thread-1"));
t2.start();
}
}
class ConcurrentFileUtil {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write(String name) {
//先获取锁
readWriteLock.writeLock().lock();
//拿到锁后
try {
System.out.println(name + " 开始写");
Thread.sleep(1000);//模拟耗时操作
System.out.println(name + " 结束写");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
System.out.println(name + " 释放了锁");
}
}
}
结果:
Thread-0 开始写
Thread-0 结束写
Thread-0 释放了锁
Thread-1 开始写
Thread-1 结束写
Thread-1 释放了锁
Condition
使用ReentrantLock
比直接使用synchronized
更安全,可以替代synchronized
进行线程同步。
但是,synchronized
可以配合wait
和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock
我们怎么编写wait
和notify
的功能呢?
答案是使用Condition
对象来实现wait
和notify
的功能。
举例:任务队列(synchronized + await + notify)
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
改写(Lock + Condition)
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
可见,使用Condition
时,引用的Condition
对象必须从Lock
实例的newCondition()
返回,这样才能获得一个绑定了Lock
实例的Condition
实例。
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
await()
会释放当前锁,进入等待状态;signal()
会唤醒某个等待线程;signalAll()
会唤醒所有等待线程;- 唤醒线程从
await()
返回后需要重新获得锁。
此外,和tryLock()
类似,await()
可以在等待指定时间后,如果还没有被其他线程通过signal()
或signalAll()
唤醒,可以自己醒来:
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
乐观锁StampedLock
悲观读锁
:读的过程不允许写。
乐观读锁
:读的过程本身不加锁,所以允许写。这样一来,我们读的数据有可能不一致,所以需要额外的代码判断读的过程中是否有写入。
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入。
要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock
。
三种模式:
- Writing:独占写锁
- Reading:悲观读锁
- Optimistic Reading:乐观读。仅当当前未处于 Writing 模式
tryOptimisticRead
才会返回非 0 的邮戳(Stamp),如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate
返回 true。
StampedLock
是不可重入锁。
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
详解乐观读带来的性能提升:
读锁未释放前,获取写锁会阻塞,写操作就不能马上执行。而乐观读锁允许写线程获取锁,写操作能马上执行。