上一节中我们介绍了jvm中的一个关键字:synchronized,这个关键字主要用于避免多线程同时操作共享资源而引起的并发问题。线程A在访问synchronized修饰的方法或者代码块之前,必须先拿到这个锁才能进行访问,这个时候线程B如果需要访问这个方法或者代码块则需要线程A释放锁才行。
但是,这样会引起一个问题,就是如果线程A一直持有这个锁,那么后面所有等待的线程都只能在这等着,这就很影响效率了。
因此就需要一种机制可以不让等待的线程一直等待下去,通过Lock就可以办到。
再举个例子:当有多个线程同时进行读写文件时,读和写操作会发生冲突,写和写操作也会发生冲突,而读和读操作时没有问题的。
这个时候如果用synchronized来实现同步的话,就会导致同时只有一个线程能进行读操作,这个也就很影响效率了。
另外,通过Lock可以知道有没有成功获取到锁,这个是synchronized无法做到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意一下两点:
- Lock不是Java内置的,是JDK1.5引入的一个类,而synchronized是JVM的一个关键字。
- Lock使用过后一定要释放锁,如果不释放锁有可能引起死锁,而synchronized用完之后由系统自动去释放这个锁。
Lock相关锁的使用
1. 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():void,最常用到的方法,用来获取锁,如果被其他线程持有则等待。前面讲到,使用lock一定要释放锁,并且在发生异常时不会释放锁,所以同步代码块一定要放在try中,并且在finally中去unlock()。
Lock lock = ...;
lock.lock();
try{
//同步代码块
}
catch(Exception e){
//处理异常
}
finally{
lock.unlock();
}
tryLock():boolean,与lock方法的区别就是这个方法是有返回值的,表示获取锁有没有成功,获取成功则返回true,获取失败返回false。这样如果获取锁失败,就不必一直等待了,可以去做别的事情。
tryLock(long time, TimeUnit unit):boolean,和tryLock类似,只不过区别在于,这个方法如果直接拿到锁就返回true,如果没有拿到,那么就等待一段时间,经过time时间之后,如果还没有拿到就返回false,拿到了就返回true。
Lock lock = ...;
if(lock.tryLock()){
try{
//同步代码块
}
catch(Exception e){
}
finally{
lock.unlock();
}
}
else{
//do something
}
lockInterruptibly():void,表示获取锁,但是这个线程能够响应中断,获取锁的过程是可以被打断、终止的。也就是说,当两个线程同时通过lockInterruptibly去获取锁,如果线程A已经获取到了锁,而线程B处于等待状态时是可以通过threadB.interrupt()去中断等待的。
Lock lock = ...;
lock.lockInterruptibly();
try{
}
catch(){
}
finally{
lock.unlock();
}
注意:当一个线程已经获取到了锁,这个时候线程是不能被interrupt的,因为interrupt不能终止正在运行的线程,只能中断阻塞的线程。
2. ReentrantLock
中文翻译为“可重入锁”,关于这个概念下一节来讲。ReentrantLock是唯一实现了Lock接口的实现类。下面通过一个实例来看下ReentrantLock的使用方法:
例子1:使用lock方法
public class LockDemo {
private Lock lock = new ReentrantLock();
private void insert(String threadName) {
lock.lock();
try {
System.out.println(threadName + "获得了锁");
for (int i = 0; i < 3; i++) {
System.out.println(threadName + "--" + i);
}
} catch (Exception e) {
} finally {
System.out.println(threadName + "释放了锁");
lock.unlock();
}
}
public static void main(String[] args) {
final LockDemo demo = new LockDemo();
Thread thread1 = new Thread() {
@Override
public void run() {
super.run();
demo.insert("Thread1");
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
super.run();
demo.insert("Thread2");
}
};
thread1.start();
thread2.start();
}
}
输入结果:
Thread1获得了锁
Thread1--0
Thread1--1
Thread1--2
Thread1释放了锁
Thread2获得了锁
Thread2--0
Thread2--1
Thread2--2
Thread2释放了锁
例子2:使用tryLock()方法
private void insert(String threadName) {
if (lock.tryLock()) {
try {
System.out.println(threadName + "获得了锁");
for (int i = 0; i < 3; i++) {
System.out.println(threadName + "--" + i);
}
} catch (Exception e) {
} finally {
System.out.println(threadName + "释放了锁");
lock.unlock();
}
} else {
System.out.println(threadName + "尝试获取锁失败,可以嘿嘿嘿了");
}
}
输出结果:
Thread1获得了锁
Thread2尝试获取锁失败,可以嘿嘿嘿了
Thread1--0
Thread1--1
Thread1--2
Thread1释放了锁
例子3:使用lockInterruptibly
private void insert(String threadName) throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println(threadName + "获得了锁");
Thread.sleep(1000);
for (int i = 0; i < 3; i++) {
System.out.println(threadName + "--" + i);
}
} catch (Exception e) {
} finally {
System.out.println(threadName + "释放了锁");
lock.unlock();
}
}
public static void main(String[] args) {
final LockDemo demo = new LockDemo();
Thread thread1 = new Thread() {
@Override
public void run() {
super.run();
try {
demo.insert("Thread1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
super.run();
try {
demo.insert("Thread2");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
e.printStackTrace();
}
}
};
thread1.start();
thread2.start();
try{
Thread.sleep(100);
thread1.interrupt();
thread2.interrupt();
}
catch (Exception e){
}
}
输出结果:
Thread1获得了锁
Thread1释放了锁
Thread-1被中断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.program.LockDemo.insert(LockDemo.java:12)
at com.program.LockDemo.access$000(LockDemo.java:8)
at com.program.LockDemo$2.run(LockDemo.java:45)
3. ReadWriteLock
ReadWriteLock和Lock一样,也是一个接口。只有两个方法,一个获取读锁,一个获取写锁。也就是说将读写操作分开来上锁,多个线程可以同时读,但是不能同时写和同时读&写。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
4. ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口,最主要就两个方法,readLock&writeLock用来获取读写锁。
下面用一个例子来看下ReentrantReadWriteLock的使用:
public class LockDemo {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private void get(String threadName) {
lock.readLock().lock();
try {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + "正在读取");
}
System.out.println(threadName + "读取结束======");
} catch (Exception e) {
} finally {
lock.readLock().unlock();
}
}
private void put(String threadName) {
lock.writeLock().lock();
try {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + "正在写入");
}
System.out.println(threadName + "写入结束=====");
} catch (Exception e) {
} finally {
lock.writeLock().unlock();
}
}
public static void main(String[] args) {
final LockDemo demo = new LockDemo();
Thread thread1 = new Thread() {
@Override
public void run() {
super.run();
demo.get("Thread1");
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
super.run();
demo.put("Thread2");
}
};
Thread thread3 = new Thread() {
@Override
public void run() {
super.run();
demo.put("Thread3");
}
};
Thread thread4 = new Thread() {
@Override
public void run() {
super.run();
demo.get("Thread4");
}
};
thread1.start();
thread2.start();
thread4.start();
thread3.start();
}
上面的代码中,有两个操作,分别是get和put,用来模拟读写操作。然后开启两个线程来读,两个线程来写。
根据上面的理论,thread1和thread4应该是并发的,而thread2和thread3应该是串行的。
但是,But
Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1正在读取
Thread1读取结束======
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2写入结束=====
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3写入结束=====
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4正在读取
Thread4读取结束======
我试了很多次,thread1和thread4都没有并发过。这不是坑吗?
我调整一下线程启动顺序,将两个读线程放一块,也就是读线程之间没有插入写线程。
thread1.start();
thread4.start();
thread2.start();
thread3.start();
输出结果:
Thread4正在读取
Thread1正在读取
Thread4正在读取
Thread1正在读取
Thread1正在读取
Thread4正在读取
Thread4正在读取
Thread1正在读取
Thread4正在读取
Thread1正在读取
Thread1读取结束======
Thread4读取结束======
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2正在写入
Thread2写入结束=====
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3正在写入
Thread3写入结束=====
可以看到thread1和thread4并发读取了。
具体原因,可以参考一下ReentrantReadWriteLock有坑,小心读锁!
synchronized还是Lock?
总结来说,synchronized和Lock有以下几点不同:
- synchronized是jvm里的一个关键字,而Lock是JDK1.5之后提供的一个上层接口。
- synchronized同步结束之后系统会自动释放锁,而Lock需要手动释放锁。
- 使用Lock锁的线程在等待过程中可以被中断,提高效率。
- 使用Lock锁,线程可以拿到是否成功获取锁的回调,不必等待。
- 使用Lock锁,可以提高多个线程同时读取的效率。
在性能上说,如果竞争资源不激烈,可以直接使用synchronized,更加方便。而如果竞争资源很激烈(也就是有大量线程竞争),这个时候使用Lock更加高效。
锁的相关概念
1.可重入锁
如果锁具备可重入性,那么这个锁就是可重入锁。synchronized和Lock都是可重入锁。其实可重入性表明了锁的分配机制:基于线程分配,而不是基于方法调用的分配。举个🌰
pubic synchronized void methodA(){
methodB();
}
pubic synchronized void methodB(){
}
我们想一下,某个时刻,线程A执行到了methodA方法中,线程A已经拿到了这个对象锁。如果,锁不具有可重入性,那么访问方法B需要重新申请锁,但是这个锁就在自己身上啊,自己又没有释放,这样就会死锁啦。
由于synchronized和Lock都具备可重入性,所以不会出现上面这种问题。
2.可中断锁
顾名思义,这个锁是可以被中断。这里不是说锁可以被中断,其实指的是在等待获取这个锁的线程是可以被中断的。
如果某一个线程A正在执行同步代码,而线程B只能等待着获取锁,如果突然灵机一动,不想等了,可以中断这个等待的过程。
synchronized是不能被中断的,Lock是可中断锁。
3.公平锁
公平锁,顾名思义,就是看这个锁是否公平了。什么意思呢?就是比如多个线程都在争取这个锁,那这个锁到底给谁呢?
公平锁,就是尽量按照请求锁的顺序来执行,即谁先请求的,就给谁,一碗水端平。
非公平锁,就是无法保证这个锁到底给谁,这样就可能导致某个线程或者某些线程一直获取不到锁,这也太不公平了,但是这个世界就是这样,人间不值得。
synchronized就是非公平锁,无法保证获取锁的顺序。Lock默认也是非公平锁,但是通过ReentrantLock和ReentrantReadWriteLock的构造参数来设置成公平锁。
4.读写锁
读写锁将对共享资源的访问分为读锁和写锁,这样既能保证多个线程可以同时读取,也可以保证不能同时写入和同时读&写。ReadWriteLock就是读写锁,上面已经介绍过啦。