本章内容并没有特别之处,主要目的是为了整理自己学习笔记,作为一个巩固和更深刻的认识。方便后面自己查阅,也供一些朋友参考
当然了本章的内容学习,需要先了解线程同步,线程锁的内容后再学习。
也就是上一期的篇章,可供参考。
什么是线程同步?什么是线程安全?什么是线程锁?synchronized怎么用?如何理解wait()和sleep()的区别?超详细例程讲解-------手摸手教会小白
上一章我们提到synchronized使用的几个案例,它们特性是这样的。当一个线程在执行带锁的方法时,其他需要使用这个方法的线程只能等待 ,等待获取锁的线程释放锁。
序号 | 获取锁的线程释放锁的两种情况: |
---|---|
1 | 获取锁的线程执行完了该代码块,然后线程释放对锁的占有. |
2 | 线程执行发生异常,此时JVM会让线程自动释放锁。 |
如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这十分影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下:Lock提供了比synchronized更多的功能。但是要注意以下两点:
(1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
(2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回 |
方法名称 | 描述 |
---|---|
void lock() | 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。 |
void lockInterruptibly() | 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断。 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
boolean tryLock() | 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。 |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false. |
void unlock() | 释放锁。 |
由于在前面讲到如果采用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是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
ReentrantLock类常见方法:
构造方法:
方法名称 | 描述 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock的实例。 |
ReentrantLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例 |
ReentrantLock类常见方法(不包括接口中的方法):
方法名称 | 描述 |
---|---|
int getHoldCount() | 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。 |
protected Thread getOwner() | 返回当前拥有此锁的线程,如果不拥有,则返回 null |
protected Collection getQueuedThreads() | 返回包含可能正在等待获取此锁的线程的集合 |
int getQueueLength() | 返回等待获取此锁的线程数的估计。 |
protected Collection getWaitingThreads(Condition condition) | 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。 |
int getWaitQueueLength(Condition condition) | 返回与此锁相关联的给定条件等待的线程数的估计。 |
boolean hasQueuedThread(Thread thread) | 查询给定线程是否等待获取此锁。 |
boolean hasQueuedThreads() | 查询是否有线程正在等待获取此锁。 |
boolean hasWaiters(Condition condition) | 查询任何线程是否等待与此锁相关联的给定条件 |
boolean isFair() | 如果此锁的公平设置为true,则返回 true 。 |
boolean isHeldByCurrentThread() | 查询此锁是否由当前线程持有。 |
boolean isLocked() | 查询此锁是否由任何线程持有。 |
ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口就是为了解决这个问题。
读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
ReadWriteLock接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
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接口
方法名称 | 描述 |
---|---|
void await() | 相当于Object类的wait方法 |
boolean await(long time, TimeUnit unit) | 相当于Object类的wait(long timeout)方法 |
signal() | 相当于Object类的notify方法 |
signalAll() | 相当于Object类的notifyAll方法 |
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()代码获得同步监视器,不然会报错。
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()的线程。
线程的交叉调用可能产生死锁,因此所有线程按顺序使用锁的话,是可以避免死锁的。但是实际上不这么做,因为这需要知道所有需要排序的锁,且对团队协作开发带来很大的难度。
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的顺序依次执行
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在同时进行读操作。这样就大大提升了读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
// An highlighted block
var foo = 'bar';
// An highlighted block
var foo = 'bar';
// An highlighted block
var foo = 'bar';
Lock锁分为:公平锁 和 非公平锁。
公平锁:表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
非公平锁:是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。
/**
* 根据ReentrantLock创建时给定不同的参数
*/
public class FairorNofairLock {
public static void main(String[] args) throws InterruptedException {
final Service service = new Service(true);//true为公平锁,false为非公平锁
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("★线程" + Thread.currentThread().getName()
+ "运行了");
service.serviceMethod();
}
};
Thread[] threadArray = new Thread[10];
for (int i = 0; i < 10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i = 0; i < 10; i++) {
threadArray[i].start();
}
}
static public class Service {
private ReentrantLock lock;
public Service(boolean isFair) {
super();
lock = new ReentrantLock(isFair);
}
public void serviceMethod() {
lock.lock();
try {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ "获得锁定");
} finally {
lock.unlock();
}
}
}
}
使用公平锁时的结果
★线程Thread-0运行了
★线程Thread-4运行了
ThreadName=Thread-0获得锁定
★线程Thread-1运行了
★线程Thread-2运行了
★线程Thread-3运行了
★线程Thread-7运行了
★线程Thread-9运行了
★线程Thread-8运行了
★线程Thread-6运行了
ThreadName=Thread-4获得锁定
ThreadName=Thread-1获得锁定
★线程Thread-5运行了
ThreadName=Thread-2获得锁定
ThreadName=Thread-3获得锁定
ThreadName=Thread-7获得锁定
ThreadName=Thread-9获得锁定
ThreadName=Thread-8获得锁定
ThreadName=Thread-6获得锁定
ThreadName=Thread-5获得锁定
可以看到线程开启的顺序时
0-4-1-2-3-7-9-8-6-5
获得锁的顺序也是
0-4-1-2-3-7-9-8-6-5
使用非公平锁时的结果
★线程Thread-0运行了
ThreadName=Thread-0获得锁定
★线程Thread-4运行了
★线程Thread-1运行了
ThreadName=Thread-4获得锁定
ThreadName=Thread-1获得锁定
★线程Thread-2运行了
★线程Thread-3运行了
ThreadName=Thread-2获得锁定
★线程Thread-6运行了
ThreadName=Thread-6获得锁定
★线程Thread-5运行了
ThreadName=Thread-5获得锁定
★线程Thread-9运行了
ThreadName=Thread-9获得锁定
ThreadName=Thread-3获得锁定
★线程Thread-7运行了
ThreadName=Thread-7获得锁定
★线程Thread-8运行了
ThreadName=Thread-8获得锁定
很明显线程开启的顺序和获得锁定的顺序不一致。
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
class Demo{
public synchronized void method1() {
method2();
}
public synchronized void method2() {
//........
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
本章例程审核通过后将附上。
下一张将介绍一下乐观锁Atomic原理。