原文链接:https://blog.csdn.net/qq_39150049/article/details/112798911
目录
一.synchronized的缺陷
上一章我们提到synchronized使用的几个案例,它们特性是这样的。当一个线程在执行带锁的方法时,其他需要使用这个方法的线程只能等待 ,等待获取锁的线程释放锁。
序号 | 获取锁的线程释放锁的两种情况: |
---|---|
1 | 获取锁的线程执行完了该代码块,然后线程释放对锁的占有. |
2 | 线程执行发生异常,此时JVM会让线程自动释放锁。 |
如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这十分影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下:Lock提供了比synchronized更多的功能。但是要注意以下两点:
(1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
(2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
二.Lock接口的特性及基本方法
接口的特性
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回 |
接口基本的方法:
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
当一个线程获取了锁之后,是不会被interrupt()方法中断的。
三、ReentrantLock介绍及实例
ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
ReentrantLock类常见方法:
构造方法:
方法名称 | 描述 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock的实例。 |
ReentrantLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例 |
ReentrantLock类常见方法(不包括接口中的方法):
lock声明为局部变量,使用lock:
public class TestReentrantLock1 {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final TestReentrantLock1 test = new TestReentrantLock1();
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 = new ReentrantLock(); //注意这个地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<1000;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
执行结果
Thread-0得到了锁
Thread-1得到了锁
Thread-0释放了锁
Thread-1释放了锁
我们会看到这样的执行结果,如果结果不是这样的,可以把循环加长一些就可以看到效果了。这是因为每个线程都new出了一个锁,因此他们互不影响,没有满足线程安全的需求。
lock声明为类的属性,使用lock
public class TestReentrantLock2 {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方
public static void main(String[] args) {
final TestReentrantLock2 test = new TestReentrantLock2();
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();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<1000;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
执行结果
Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁
当Lock申明为类属性的时候,两个线程只创建一个锁,因此是同一把锁,可以满足线程安全的需求。
trylock的使用
public class TestReentrantLock3 {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方
public static void main(String[] args) {
final TestReentrantLock3 test = new TestReentrantLock3();
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) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<100000;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"获取锁失败");
}
}
}
执行结果
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
这个例程可以看到,线程1在尝试获取锁的时候由于,线程0正在使用,因此尝试失败了。
lockInterruptibly的使用
public class TestReentrantLock4 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
TestReentrantLock4 test = new TestReentrantLock4();
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()+"得到了锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= 3999)
break;
//插入数据
}
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private TestReentrantLock4 test = null;
public MyThread(TestReentrantLock4 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释放了锁
四、ReadWriteLock接口
ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口就是为了解决这个问题。
读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
ReadWriteLock接口的方法:
方法名称 | 描述 |
---|---|
Lock readLock() | 共享锁,可以多个线程同时读 |
Lock writeLock() | 互斥锁,有一个线程在写,其他线程不能写也不能读,只能等待 |
也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
五、ReentrantReadWriteLock
ReentrantReadWriteLock实现了上面讲的ReadWriteLock接口
ReentrantReadWriteLock的特性:
读读共享
/**
* 两个读共享锁
*/
public class ReadRead {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReadRead test = new ReadRead();
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();
}
}
}
此时打印的结果为(每次结果不同):
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0正在进行读操作
Thread-0读操作完毕
说明thread1和thread2在同时进行读操作。这样就大大提升了读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
六、Condition接口
简介
synchronized关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现 等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。
而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
Condition接口的常见方法:
使用单个Condition实例实现等待/通知机制:
public class UseSingleConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() throws InterruptedException {
lock.lock();
try {
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("这是condition.signal()方法之后的语句");
} finally {
lock.unlock();
}
}
}
static public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
}
执行结果
await时间为1612080501270
signal时间为1612080504272
这是condition.signal()方法之后的语句
这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行
可以看到,await()方法和signal()方法实现了与wait()、notify()同样的功能。
注意: 必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。
使用多个Condition实例实现等待/通知机制:
public class UseMoreConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyserviceMoreCondition service = new MyserviceMoreCondition();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(3000);
service.signalAll_A();
}
static public class ThreadA extends Thread {
private MyserviceMoreCondition service;
public ThreadA(MyserviceMoreCondition service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
static public class ThreadB extends Thread {
private MyserviceMoreCondition service;
public ThreadB(MyserviceMoreCondition service) {
super();
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
}
MyserviceMoreCondition
public class MyserviceMoreCondition {
private Lock lock = new ReentrantLock();
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
public void awaitA() {
lock.lock();
try {
System.out.println("begin awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.await();
System.out.println(" end awaitA时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
lock.lock();
try {
System.out.println("begin awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.await();
System.out.println(" end awaitB时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalAll_A() {
lock.lock();
try {
System.out.println(" signalAll_A时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
lock.lock();
try {
System.out.println(" signalAll_B时间为" + System.currentTimeMillis()
+ " ThreadName=" + Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
}
执行结果
begin awaitA时间为1612084608495 ThreadName=A
begin awaitB时间为1612084608512 ThreadName=B
signalAll_A时间为1612084611512 ThreadName=main
end awaitA时间为1612084611512 ThreadName=A
从结果上可以看出只有A线程被唤醒了。明显的signalAll()与notifyAll()不一样,它没有唤醒全部wait()的线程。
使用Condition实现顺序执行
线程的交叉调用可能产生死锁,因此所有线程按顺序使用锁的话,是可以避免死锁的。但是实际上不这么做,因为这需要知道所有需要排序的锁,且对团队协作开发带来很大的难度。
public class ConditionSeqExec {
volatile private static int nextPrintWho = 1;
private static ReentrantLock lock = new ReentrantLock();
final private static Condition conditionA = lock.newCondition();
final private static Condition conditionB = lock.newCondition();
final private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Thread threadA = new Thread() {
public void run() {
try {
lock.lock();
//如果nextPrintWho不为1就等待
while (nextPrintWho != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.print("ThreadA " + (i + 1)+" " );
}
System.out.println();
nextPrintWho = 2;
//通知conditionB实例的线程运行
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadB = new Thread() {
public void run() {
try {
lock.lock();
//如果nextPrintWho不为2就等待
while (nextPrintWho != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.print("ThreadB " + (i + 1)+" " );
}
System.out.println();
nextPrintWho = 3;
//通知conditionC实例的线程运行
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadC = new Thread() {
public void run() {
try {
lock.lock();
//如果nextPrintWho不为3就等待
while (nextPrintWho != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.print("ThreadC " + (i + 1)+" " );
}
System.out.println();
nextPrintWho = 1;
//通知conditionA实例的线程运行
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread[] aArray = new Thread[5];
Thread[] bArray = new Thread[5];
Thread[] cArray = new Thread[5];
for (int i = 0; i < 5; i++) {
aArray[i] = new Thread(threadA);
bArray[i] = new Thread(threadB);
cArray[i] = new Thread(threadC);
aArray[i].start();
bArray[i].start();
cArray[i].start();
}
}
}
运行结果
ThreadA 1 ThreadA 2 ThreadA 3
ThreadB 1 ThreadB 2 ThreadB 3
ThreadC 1 ThreadC 2 ThreadC 3
ThreadA 1 ThreadA 2 ThreadA 3
ThreadB 1 ThreadB 2 ThreadB 3
ThreadC 1 ThreadC 2 ThreadC 3
ThreadA 1 ThreadA 2 ThreadA 3
ThreadB 1 ThreadB 2 ThreadB 3
ThreadC 1 ThreadC 2 ThreadC 3
ThreadA 1 ThreadA 2 ThreadA 3
ThreadB 1 ThreadB 2 ThreadB 3
ThreadC 1 ThreadC 2 ThreadC 3
ThreadA 1 ThreadA 2 ThreadA 3
ThreadB 1 ThreadB 2 ThreadB 3
ThreadC 1 ThreadC 2 ThreadC 3
可以看到这样就实现了,线程按A->B->C->A的顺序依次执行