文章目录
什么是Lock锁
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对 象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
- Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
- Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
1. lock接口:
Look接口常用方法
public interface Lock {
void lock();//获得锁
//获取锁定,除非当前线程是interrupted
void lockInterruptibly() throws InterruptedException;
boolean tryLock();//只有在调用时才可以获取锁
//如果在给定的时间内是空闲的,并且当前的线程尚未得到释放,即可获取锁
boolean tryLock(long time, TimeUnit unit)throws InterruptedException;
void unlock();//释放锁
Condition newCondition();//返回一个新的Condition绑定到Lock实例
}
-
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。
-
unlock()方法用于释放锁。
示例:
ock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
2. ReentrantLock(可重入锁)
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。
public class ReentrantLock {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final ReentrantLock test = new ReentrantLock();
new Thread(() -> test.insert(Thread.currentThread())).start();
new Thread(() -> test.insert(Thread.currentThread())).start();
}
public void insert(Thread thread) {
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(); //注意这个地方
lock.lock();
try {
System.out.println(thread.getName() + "得到了锁");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
}
}
ReentrantLock()锁既可以是公平锁也可以是非公平锁
/**
* 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();
}
ReentrantLock()默认情况下是非公平锁状态,当传参为true时表示公平锁
3 synchronized 和 ReentrantLock 的区别
- synchronized依赖于JVM,官方在JDK1.6之后对其进行了优化,但是这些优化都在在底层实现的,没有直接暴漏出来;而ReentrantLock依赖与API,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的
- 相比于synchronized , ReentrantLock有以下几个优点:
- 等待可中断:
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁:
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 两者都是可重入锁
- 等待可中断:
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
当一个线程获得当前实例的锁lock,并且进入了方法A,该线程在方法A没有释放该锁的时候,是否可以再次进入使用该锁的方法B?
-
不可重入锁:在方法A释放锁之前,不可以再次进入方法B
-
可重入锁:在方法A释放该锁之前,可以再次进入方法B
重入进一步提升了加锁行为的封装性,因而简化了面向对象并发代码的开发。在以下程序中,子类改写了父类的 synchronized 方法,然后调用父类中的方法,此时如果内置锁不是可重入的,那么这段代码将产生死锁。由于 Widget 和 LoggingWidget 中 doSomething 方法都是 synchronized 方法,因此每个每个 doSomething 方法在执行前都会获取 Widget 上的锁。然而如果内置锁不是可重入的,那么调用 super.doSomething( )时无法获得 Widget 上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。
public class Widget{
public synchronized void doSomething(){
…
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
super.doSomething();
}
}
synchronized是隐式可重入锁,Lock是显示可重入锁
public class SyncLockDemo {
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+" 外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 内层");
}
}
}
},"t1").start();
}
}
输出结果:
t1 外层
t1 中层
t1 内层
进程已结束,退出代码为 0
在类中创建一个add方法
public synchronized void add() {
add();
}
在main方法中调用该方法
new SyncLockDemo().add();
结果出现错误:
Exception in thread "main" java.lang.StackOverflowError
at com.SyncLockDemo.add(SyncLockDemo.java:12)
at com.SyncLockDemo.add(SyncLockDemo.java:12)
原因:
synchronized是一个可重入锁,用的是同一把锁的情况下,该方法不断调用add方法,导致方法栈溢出,
public static void main(String[] args) {
//Lock演示可重入锁
ReentrantLock lock = new ReentrantLock();//非公平锁
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
}
}
输出结果:
t1 外层
t1 内层
特殊情况,注释掉try-finally中的try-finally中的释放锁
输出结果正常;
原因:Lock是可重入锁,自己操作自己可以正常运行,
在main中创建一个新线程
//创建新线程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
结果不能正常输出,因为Lock锁没有 释放,新线程无法获取锁。必须等到lock锁释放,程序才能往下走。
4.newCondition
Condition
因素出Object
监视器方法(wait
,notify
和notifyAll
)成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock
个实现。Lock
替换synchronized
方法和语句的使用,Condition
取代了对象监视器方法的使用。
一个
Condition
实例本质上绑定到一个锁。 要获得特定Condition
实例的Condition实例,请使用其newCondition()
方法。
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
• await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
• signal()用于唤醒一个等待的线程。
注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
虚假唤醒
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) //注意这里用的是while
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
当前线程调用wait的时候必须先获取到锁,而执行了wait方法之后当前线程回释放该锁并且进入进入waiting状态,直到其他线程调用nodify方法或者notifyAll方法。
我们知道Condition中的signal方法和Object类里面的notify方法效果类似,并且调用方法之前需要先获取到锁,Condition中的await方法和Object类里面的wait方法效果类似,也是调用之前需要获取到锁,而调用之后会释放锁。
注意重点调用await方法或者wait方法之后会释放掉锁,有了这个前提再看上面的代码,只看重点的while循环这一步,
...
while (count == items.length) //注意这里用的是while
notFull.await();
...
当count == items.length条件满足的时候,走到await方法,当前线程挂起,释放掉锁,这时候count变量由于没有被锁住,所以可能会有其他线程将count值修改,导致条件判断产生变化。当另一个线程调用signal唤醒此线程的时候,if和while的区别就体现出来了,如果是if的话线程被唤醒就会直接从await方法往下执行,但是这时候count可能被修改,count == items.length条件可能又满足了,这种情况应该是让线程继续挂起,但是用if会唤醒线程,产生了所谓的虚假唤醒,而如果用while的话,线程被唤醒之后还会在执行一次条件判断,确保条件count == items.length不满足的时候才唤醒线程。
5.ReadWriteLock(读写锁)
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
- Lock readLock(); //读锁
- Lock writeLock(); //写锁
5.1使用synchronized锁创建多线程同时进行读操作。
public class Test {
public static void main(String[] args) {
final Test test = new Test();
new Thread(() -> test.get(Thread.currentThread())).start();
new Thread(() -> test.get(Thread.currentThread())).start();
}
public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行读操作");
}
System.out.println(thread.getName() + "读操作完毕");
}
}
结果:
...
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
进程已结束,退出代码为 0
有结果可知:使用synchronized锁进行多线程读操作为非公平锁
5.2使用ReadReadWriteLock锁进行多线程同时进行读操作
public class Test {
private ReentrantReadWriteLock rwl = new
ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(() -> test.get(Thread.currentThread())).start();
new Thread(() -> test.get(Thread.currentThread())).start();
}
public void get(Thread thread) {
rwl.readLock().lock();//加锁
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在进行读操作");
}
System.out.println(thread.getName() + "读操作完毕");
} finally {
rwl.readLock().unlock();//释放锁
}
}
}
结果:
...
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0读操作完毕
进程已结束,退出代码为 0
说明 两个进程在同时进行读操作。这样就大大提升了读操作的效率。使用读写锁之前需要先加锁
注意:
• 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
• 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。