前言
从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock,再存在synchronized的情况下,为什么会出现Lock来实现同步呢?它和synchronized又有哪些不同呢?
synchronized的局限性
第一点:
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,这样会非常影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
第二点:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致: 如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象;
java.util.concurrent.locks包下常用的类
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接口主要有获取锁的方法和释放锁的方法
获取锁的方法:
lock() : 获取锁,如果锁已经被其他线程获取,则等待(这是我们使用最多的一个方法)
tryLock() : 尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
tryLock(long time, TimeUnit unit) :这个方法会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly() :获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程,lockInterruptibly()的声明中抛出了异常
拓展:线程的interrupt方法的作用:对于正在运行的线程,设置中断标志位true,在调用interrupted()或者isInterrupt()方法来获取中断中断状态做出选择,对于阻塞的线程(wait,sleep,join)则会清除中断状态并且抛出异常,即单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
释放锁的方法:
unlock()释放锁
使用lock锁的演示,把锁对象定义成成员属性:
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方,成员属性
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
lock.lock();//如果使用tryLock(),则把下面代码放在if(tryLock())判断里面
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
使用中断锁lockInterruptibly()的演示:线程2在执行lock.lockInterruptibly()方法时因为没有获取到锁
public class PersonTest {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
PersonTest test = new PersonTest();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(1000*4);
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private PersonTest test = null;
public MyThread(PersonTest test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
执行结果:
Thread-0得到了锁
Thread-1被中断
Thread-0执行finally
Thread-0释放了锁
ReentrantLock可重入锁(实现Lock接口)
可重入锁:如果锁具备可重入性,则称作为可重入锁。
可重入性:如果一个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作粒度是线程而不是调用,实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
重入的实现方法:为每个锁关联一个获取计数值和一个所有者线程,当计数值为0时候,这个锁认为是没有任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块的时候。计数器相应地递减,当计数器值为0时候,这个锁将被释放
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
ReentrantReadWriteLock 读写锁(实现ReadWriteLock)
ReadWriteLock接口:可以使多个线程能同时进行读操作
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();//写锁
}
ReentrantReadWriteLock实现类
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
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();
}
}
}
执行的时候,线程0和线程1交替执行读操作直至操作完毕,这样就大大的提升了效率
如果有一个线程已经占用了读锁,其他线程如果要申请读锁,也可以申请读锁,多个线程可以同时执行读操作
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁
查看ReentrantLock的源码:
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。我们可以在构造对象的时候传入boolean fair,即:
ReentrantLock lock = new ReentrantLock(true);//true表示公平锁,false非公平锁
默认的构造方法是非公平锁
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
/**
* 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其他常用的方法:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。
Lock和synchronized的区别:
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
通过Condition实现等待通知机制
在synchronized中我们有wait()和notify(),notifyAll()来实现等待通知机制,在lock实现锁的机制中我们如何实现等待通知机制呢?
我们通过Condition对象来实现等待和通知机制,我们通过Lock接口中的newCondition()方法来创建Condition对象
在Condition对象中通过:await()方法就是等待,signal()/signalAll()方法就是通知,condition可以有多个。
public interface Condition {
void await() throws InterruptedException; //等待方法
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal(); //唤醒该lock所有等待
void signalAll();//唤醒程序中所有lock的等待
}
注意:这里的signal()和signalAll()方法和synchronized的notify()/notifyAll()方法有所不同,signal()通知这个lock的所有等待,而signalAll()通知程序中所有的lock的所有等待
public class PersonTest {
public static void main(String[] args) throws InterruptedException {
myService myService=new myService();
ThreadA a=new ThreadA(myService);
a.start();
Thread.sleep(3000);
myService.signalService();
}
}
class myService {
private Lock lock=new ReentrantLock();
public Condition condition=lock.newCondition();
public void awaitService(){
try{
lock.lock();
System.out.println("await等待 ");
condition.await();
System.out.println("finish await ");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void signalService(){
try{
lock.lock();
System.out.println("signal 唤醒await");
condition.signal();
}finally{
lock.unlock();
}
}
}
class ThreadA extends Thread{
private myService myService;
public ThreadA(myService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
myService.awaitService();
}
}
执行结果:
await等待
signal 唤醒await
finish await
实现生产-消费模式:
public class PersonTest {
public static void main(String[] args) throws InterruptedException {
MyService myService=new MyService();
ThreadA a=new ThreadA(myService);
a.start();
ThreadB b=new ThreadB(myService);
b.start();
}
}
class MyService {
private Lock lock=new ReentrantLock();
private Condition condition=lock.newCondition();
private boolean hasValue=true;
public void set(){
try{
lock.lock();
while(hasValue){
condition.await();
}
System.out.println("打印#");
hasValue=true;
condition.signal();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void get(){
try{
lock.lock();
while(!hasValue){
condition.await();
}
System.out.println("打印&");
hasValue=false;
condition.signal();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
class ThreadA extends Thread{
private MyService myService;
public ThreadA(MyService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
for(int i=0;i<3;i++){
myService.set();
}
}
}
class ThreadB extends Thread{
private MyService myService;
public ThreadB(MyService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
for(int i=0;i<3;i++){
myService.get();
}
}
}
运行结果:
打印&
打印#
打印&
打印#
打印&
打印#