接上一章,本章来学习多线程的重中之重,锁!
文章目录
概述
多线程的同步问题,几乎是多线程开发中不可避免要面对的问题。
锁机制,就是为解决多线程同步问题而生。
简单来说,锁机制就是类似于现实生活中的锁。对某段代码上锁,留出一把钥匙。拿到这把钥匙的线程,就可以开锁,执行这段代码。拿不到钥匙的线程,就只能等待钥匙用完后被分享出来,再去争取得到这把钥匙来开锁。
如此往复,就能保证多个线程,轮流着执行这段代码(每个线程对这段代码的整个执行过程是不会被打断的,所以是同步的)。就不会出现多个线程同时在执行这段代码,互相打断,造成混乱的情况。一切因为上了锁而变得有序和同步。
综上,线程同步的重点就在于锁。
锁的重点就在于:
1、获取锁(得到执行的许可)
2、等待锁(维护秩序,保证执行时没有干扰)
3、释放锁(给下一个线程来执行)
多线程的锁机制,就是从这三点出发,通过各种方式,来达到锁的效果。
1、synchronized
关键字
synchronized
关键字是线程安全的一种方式,通过synchronized
可以为某块具体代码上锁,要调用到这块代码,就必须先获取到锁,获取不到,就必须等待。
-
使用
synchronized
关键字修饰的方法或代码块,就是对其加了锁。(一般都是对象锁;如果是修饰在了static
关键字上,加的就是类锁。)
在锁未被释放的时候,任何用到同一个锁的地方都无法被其他线程调用。
即,两个synchronized
方法使用了同一个对象锁,那么即使只有一个方法被线程[A]调用,其他线程[B]、[C]等也无法调用另一个方法,因为其他线程获取不到锁,锁正在被[A]占用着。 -
同步方法与同步代码块的区别:
同步方法:synchronized
关键字加在方法上,默认当前对象作为锁。
同步代码块:synchronized
关键字套在具体代码块上,需显式指定任意对象作为锁。 -
类锁和对象锁的区别:
类锁:
1、synchronized
关键字加在static
关键字上的,调用的就是类锁。
2、在同步代码块自定义锁的时候,使用类名.class
也可以调用类锁。
对象锁:
除了类锁之外的,其他都是对象锁。
各种synchronized
的实现方式如下:
public class service{
//1、synchronized同步代码块(对象锁)
public void synMethod() {
synchronized(this) { //this:调用的是当前类对象的对象锁
//do something ...
}
}
//2、synchronized同步方法(对象锁)
public synchronized void synMethod2() { //不需要显式声明,默认调用当前类对象的对象锁
//do something ...
}
//3、synchronized同步静态方法(类锁)
public static synchronized void synMethod3() { //不需要显式声明,默认调用当前类的类锁
//do something ...
}
}
-
可重入锁:(是锁在使用过程中的一种逻辑,并不是锁的特性。)
在线程获取到对象锁之后,此时这个锁还未释放,且再次需要获取这个锁(如调用了其他同步方法),那么这个锁是可重复获取的。
即在锁未释放的时候,任何用到这个锁的地方,线程都可以进入,因为锁已经获取到了。
注:可重入锁也支持在父子类继承中(子类同步方法获取到锁之后,同样可以调用父类同步方法)。 -
出现异常,锁自动释放。
-
同步方法不具备继承性:就算父类方法是
synchronized
,子类继承后,没有显式加上synchronized
关键字,子类这个方法就不是同步方法。
2、synchronized
锁
前面说到,静态同步方法,加的是类锁,该类下的任何类实例都是调用的同一个类锁。
那如果同时使用了类锁和该类对象的对象锁,这两个锁是同一个吗?答案是否定的。
每个锁都是唯一独立的,功能简单,可以仅仅看作是一把钥匙,用于开锁,除此之外无其他任何特性。
只需要保持这一个原则,其他的如:内部类、静态内部类等这些情况的锁,也能够明白了,这些锁都是唯一的。
所以就算同步方法获取了锁,同步代码块也需要等待这个锁。并不会因为不是同样的同步方式,就影响了锁的获取。
3、死锁
死锁就是永远获取不到锁,永远在等待。
造成死锁的原因有很多,举个例子:
线程A
获取了A锁
,正在等待B锁
执行接下来的程序;- 此时
线程B
已经获取了B锁
,又在等待A锁
来执行接下来的程序。 - 此时
线程A
要获取到B锁
,才能执行完,执行完才能释放A锁
; - 但是
线程B
已经占据了B锁
,又需要线程A
释放的A锁
,来让线程B
执行完,才能释放他的B锁
给线程A
使用。 线程A、B
互相等待,且永远都等待不到结果,这就造成了死锁。
代码参考:
public class TestThreadLock {
public void sync1() {
synchronized(this) {
try {
System.out.println("test1");
Thread.sleep(3000);// 休眠3秒,以让步给线程b的sync2方法,让其获取到类锁
synchronized(TestThreadLock.class) {
System.out.println("test11");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void sync2() {
synchronized(TestThreadLock.class) {
try {
System.out.println("test2");
Thread.sleep(3000); // 休眠3秒,以让步给线程a的sync1方法,让其获取到对象锁
synchronized(this) {
System.out.println("test21");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestThreadLock t = new TestThreadLock();
Thread a = new Thread(new Runnable() {
public void run() {
t.sync1();
}
});
Thread b = new Thread(new Runnable() {
public void run() {
t.sync2();
}
});
a.start();
b.start();
}
}
执行结果:
test1
test2
执行解析:
1、线程a调用的sync1方法上,有两个同步代码块,分别调用了当前对象锁和类锁(线程b同样);
2、线程a先执行,调用sync1,获取到当前对象t的对象锁,打印日志,休眠3秒;
3、在线程a休眠的时候,线程b开始执行,调用sync2,获取到类锁,打印日志,休眠3秒;
4、线程b休眠,又轮到了线程a继续执行,此时线程a需要获取到类锁,才能接着执行,但是类锁在线程b上,还未释放,线程a等待;
5、cpu切换,轮到线程b执行,此时线程b需要获取到当前对象t的对象锁,才能接着执行,但是对象锁在线程a上,还未释放,线程b等待;
6、cpu继续切换,又轮到线程a,又是同样的情况,继续等待类锁;
7、cpu继续切换,又轮到线程b,还是同样的情况,继续等待对象锁;
8、如此循环往复,线程a、b在互相等待对方的锁,但双方都释放不了自己的锁,这就造成了死锁;
9、所以控制台只打印了两条日志,就没有了后续,因为程序已经陷入死锁。
所以在使用锁的时候,也要尤其小心,避免死锁的情况。必须确保锁最终能够被释放。
但很多时候,我们的代码逻辑并没有像上面的例子一样清晰简单,在复杂的逻辑下,造成死锁会变得很难查证问题。
这时候可以借用工具辅助检测(Jconsole、Jstack)。
4、Lock
锁
除了synchronized
关键字,还有Lock
接口可以实现锁。
同为锁,Lock
与synchronized
的区别有:
- 从定义上,
synchronized
是java关键字,是jvm实现的。而Lock
是由jdk实现的一个类,并不是内置的。 - 从使用上,
synchronized
是自动加锁、释放锁,遇到异常程序会自动释放。而Lock
的加锁、解锁、获取等,每一步都需要自己手动操作,遇到异常,也需要自己处理锁的释放。
相比之下,Lock
使用起来需要更多操作,但也提供了更多的灵活性。
-
Lock
锁的一般用法:lock()
:获取锁,如果锁已被其他线程获取,则进行等待。unLock()
:释放锁。tryLock()
:有返回值的获取锁。如果获取成功,则返回true
,如果获取失败,则返回false
。无论获不获取到锁,都会立即返回,就算拿不到锁也不会一直等待。tryLock(long time, TimeUnit unit)
:类似tryLock()
,只是多了等待时间。在设置的time
等待时间内,获取到锁了,就返回true
;一直获取不到锁,就返回false
。newCondition()
:等待/唤醒的配合,下一节介绍。
-
发生异常时,程序不会自动释放锁。所以一般使用
lock()
时必须在try{}catch{}
块中进行,并且将释放锁的操作放在finally
块中进行,以保证锁一定被被释放,防止死锁的发生。
使用例子://lock() 的使用 ======================== Lock lock = new ReentrantLock(); lock.lock();//获取锁 try{ //do something... }catch (Exception e){ e.printStackTrace(); }finally{ lock.unlock();//释放锁 } //tryLock() 的使用 ======================== Lock lock = new ReentrantLock(); if(lock.tryLock(1, TimeUnit.SECONDS)){//设置获取锁的等待时长1秒 try{ //do something... }catch (Exception e){ e.printStackTrace(); }finally{ lock.unlock();//释放锁 } }else{ //do something ... }
ReentrantLock
是Lock
的具体实现。
-
ReentrantLock
:实现了常规的Lock
方法,用以进行锁的调用。
new ReentrantLock()
:新建锁对象,默认为 非公平锁;
new ReentrantLock(boolean isFair)
:新建锁对象,true
=公平锁,false
=非公平锁。
公平锁:按照先来先得(FIFO)的顺序,分配锁。
非公平锁:随机分配锁。
举个例子://公平锁 public class TestThreadReentrantLock { private ReentrantLock lock; //通过初始化,创建锁 public TestThreadReentrantLock(boolean isFair) { lock = new ReentrantLock(isFair); } //具体演示加锁释放锁的方法 public void server() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "获得了锁"); }finally { lock.unlock(); } } //创建5个线程,演示公平/非公平锁 public static void main(String[] args) { TestThreadReentrantLock t = new TestThreadReentrantLock(true); //输入true,创建公平锁 Runnable r = new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + "开始运行"); t.server(); } }; Thread[] threads = new Thread[5]; for(int i = 0; i < 5; i++) { threads[i] = new Thread(r); } for(int i = 0; i < 5; i++) { threads[i].start(); } } } 执行结果: Thread-0开始运行 Thread-4开始运行 Thread-3开始运行 Thread-1开始运行 Thread-2开始运行 Thread-0获得了锁 Thread-4获得了锁 Thread-3获得了锁 Thread-1获得了锁 Thread-2获得了锁 执行解析: 1、从结果可以看出,前5行是线程开始运行的日志,后5行是线程轮流获取锁的日志; 2、获取锁的顺序,就是线程运行的顺序; 3、这就是公平锁,根据线程进入队列的顺序,来公平地配给锁。 //非公平锁 1、将main()方法里创建测试类的入参改为false: TestThreadReentrantLock t = new TestThreadReentrantLock(false); //输入false,创建非公平锁 2、重新运行程序 3、执行结果: Thread-0开始运行 Thread-3开始运行 Thread-0获得了锁 Thread-1开始运行 Thread-4开始运行 Thread-2开始运行 Thread-1获得了锁 Thread-3获得了锁 Thread-4获得了锁 Thread-2获得了锁 4、从结果可以发现,运行顺序是:0-3-1-4-2,获取锁顺序是:0-1-3-4-2 5、非公平锁,获取锁的顺序是随机的,与线程进入队列的顺序无关
-
ReentrantReadWriteLock
:读写锁ReentrantReadWriteLock.readLock()
:读操作相关的锁,也称为共享锁。
同一时间可以有多个线程同时读取锁后的内容。
所以加锁的意义在于,在读取的时候,就不能进行写入了,保证读取数据的实时正确性。因为读锁的存在,导致在写入前调用的写锁,会获取不到。直到读完,解锁,才可以写。ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); lock.readLock().unlock();
ReentrantReadWriteLock.writeLock()
:写操作相关的锁,也称为排他锁。只要有写锁,就是互斥的。lock.writeLock().lock(); lock.writeLock().unlock();
读写锁的意义:
单纯从读写锁的使用上来看,好像没必要使用他们。因为读锁可以重入,那么不加锁就好。写锁互斥,那么加一般的锁也一样。
那为什么还会设计出读写锁来呢?
关键的地方在于:
1、读与写,用的是同一个锁。
2、读操作和写操作,存在并发调用的情况。
正是因为这同一个锁,用在了读和写之上,才可以将读、写统一起来管理。当读与写同时发生时,因为用的是同一个锁,读写互斥,所以读和写必定只能先执行完一个,才能执行下一个。这样就避免了,在读出数据的时候,又重新写入了数据,导致读出的数据变成了脏数据。
同时,读写锁提供的读读不互斥,又使得在进行读操作时,可以同步读取数据,不会阻塞。提高了效率。
这样的读、写配合,为相关的读写操作,提供了更为便利和高效的锁控制。举个例子,加深印象:
/** * 1、设计一个读方法,加读锁; * 2、设计一个写方法,加写锁; * 3、创建多个线程,看读读、写写、读写 之间的效果。 */ public class TestThreadReadWriteLock { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //读 public void read() { try { lock.readLock().lock(); System.out.println(new Date() + "--" + Thread.currentThread().getName() + " - read "); Thread.sleep(1000); // 休眠一秒,以查看是重入还是等待 的效果 }catch(Exception e) { e.printStackTrace(); }finally { lock.readLock().unlock(); } } //写 public void write() { try { lock.writeLock().lock(); System.out.println(new Date() + "--" + Thread.currentThread().getName() + " - write "); Thread.sleep(1000); // 休眠一秒,以查看是重入还是等待 的效果 }catch(Exception e) { e.printStackTrace(); }finally { lock.writeLock().unlock(); } } public static void main(String[] args) { TestThreadReadWriteLock t = new TestThreadReadWriteLock(); int length = 10; Thread[] readThreads = new Thread[length];//读线程 Thread[] writeThreads = new Thread[length];//写线程 for(int i = 0; i < length; i++ ) { Thread readThread = new Thread(new Runnable() { public void run() { t.read(); } }); readThreads[i] = readThread; Thread writeThread = new Thread(new Runnable() { public void run() { t.write(); } }); writeThreads[i] = writeThread; } try { //读读不互斥 System.out.println("--- 读读不互斥 ---"); readThreads[0].start(); readThreads[1].start(); readThreads[4].start(); readThreads[5].start(); Thread.sleep(8000); //休眠8秒,等前面的线程都执行完毕,再执行下一组测试 //写写互斥 System.out.println("--- 写写互斥 ---"); writeThreads[0].start(); writeThreads[1].start(); writeThreads[4].start(); Thread.sleep(8000); //读写互斥 System.out.println("--- 读写互斥 ---"); readThreads[2].start(); writeThreads[2].start(); readThreads[6].start(); writeThreads[5].start(); readThreads[7].start(); }catch(Exception e) { e.printStackTrace(); } } } 执行结果: --- 读读不互斥 --- Thu Jun 18 09:39:07 CST 2020--Thread-2 - read Thu Jun 18 09:39:07 CST 2020--Thread-10 - read Thu Jun 18 09:39:07 CST 2020--Thread-8 - read Thu Jun 18 09:39:07 CST 2020--Thread-0 - read --- 写写互斥 --- Thu Jun 18 09:39:15 CST 2020--Thread-1 - write Thu Jun 18 09:39:16 CST 2020--Thread-3 - write Thu Jun 18 09:39:17 CST 2020--Thread-9 - write --- 读写互斥 --- Thu Jun 18 10:55:54 CST 2020--Thread-12 - read Thu Jun 18 10:55:54 CST 2020--Thread-4 - read Thu Jun 18 10:55:55 CST 2020--Thread-5 - write Thu Jun 18 10:55:56 CST 2020--Thread-11 - write Thu Jun 18 10:55:57 CST 2020--Thread-14 - read 执行解析: 0、从上面打印的日志可以看出: 1、“读读不互斥”日志里:每个读锁基本都是同一时间获得,并不需要等待读锁方法里休眠时间的结束。 说明,读锁是可重入的。 2、“写写互斥”日志里:每个写锁,进入时间都不同,都是等待上一个写锁进入1秒(写锁方法的休眠时间)后,再进入。 说明,写锁是互斥的。 3、“读写互斥”日志里:因为线程是按照读、写、读、写、读的顺序开启的。所以读写锁的日志理应是交错的。 但实际上,日志先打印了两个读操作,隔了1秒后,才打印了写操作。 说明,当读写操作并发时,读操作可以重入,但写操作,必须等锁释放了,才可以进入。 尽管读、写操作调用的叫读锁和写锁,但本质上其实是同一个锁。只是为不同的操作,有不同的效果。如读操作,是可重入的效果。写操作,是互斥的效果。 再继续看日志,可以发现,写操作1秒后,又开始下一个写操作,隔1秒后,再执行最后一个读操作。 再一次说明,读、写操作之间是互斥的,必须得等到一个操作释放锁了,才能获取到锁进行下一个操作。
-
lock
与synchronized
的区别
1. 用法:synchronized
既可以很方便的加在方法上,也可以加载特定代码块上,而lock
需要显示地指定起始位置和终止位置。
2. 实现:synchronized
是依赖于JVM实现的,而ReentrantLock
是JDK实现的。
3. 性能:synchronized
和lock
其实已经相差无几,其底层实现已经差不多了。
4. 功能:ReentrantLock
功能更强大,可操作性更高。ReentrantLock
可以指定是公平锁还是非公平锁,而synchronized
只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。ReentrantLock
提供了一个Condition
类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized
要么随机唤醒一个线程要么唤醒全部线程。
注:在遇到需要控制线程同步的时候,建议优先考虑synchronized
,如果有特殊需求,再考虑ReentrantLock
。因为ReentrantLock
中如锁的释放等,都需要手动操作,容易引起问题。如果用的不好,不仅不能提高性能,还可能带来灾难。
5、Condition
(对象监视器)
使用Condition
可以对Lock
锁进行等待/唤醒。类似于synchronized
和wait/notify
的关系,Condition
是属于Lock
的等待/唤醒机制。
比起synchronized
更灵活的是,Condition
可以为每个线程,各自注册指定的Condition
(可以一对一,也可以一对多),从而实现对指定线程的等待和唤醒。
而synchronized
的等待唤醒,是随机的。可以看作是只有一个Condition
,这个Condition
上注册了所有的线程,notify/notifyAll
的时候,也只有随机唤醒这之中的一个线程。
因而相比之下,Condition
与Lock
的结合,为线程的等待/唤醒,提供了更高的可操作性。
创建:
- 依赖于
Lock
接口,通过lock.newCondition()
新建。 - 调用
Condition
的方法前,必须先lock()
获得锁,否则会抛出IllegalMonitorStateException
异常。
主要方法:
Condition.await()
: 类似于Object.wait()
,释放锁,并加入等待队列。Condition.signal()
: 类似于Object.notify()
,唤醒。 根据等待队列先进先出(FIFO)原则,按顺序进行唤醒。Condition.signalAll()
:类似Object.notifyAll()
,对等待队列中的每个线程均执行一次signal()
方法,将等待队列中的线程全部移动到阻塞队列中,并唤醒队列中的每个线程。
原理:
Condition
是AbstractQueuedSynchronizer的内部类,AbstractQueuedSynchronizer类里有一个阻塞队列和n个等待队列。
阻塞队列存放着等待锁(等待运行)的线程,头部线程是正在运行的线程,按照FIFO原则,顺序执行队列里的线程。
等待队列存放着调用了await()
的线程,每个Condition
都有属于自己的等待队列。
- 当
Condition
调用await()
方法时,就会将当前线程存入到Condition
的等待队列尾部,同时释放锁,等待唤醒。这时候,线程就与Condition
建立起了联系。 - 当
Condition
调用signal()
方法时,会将等待队列的第一个线程(按照FIFO原则)加入到阻塞队列尾部,等待被唤醒,获取锁。 - 当
Condition
调用signalAll()
方法时,也是类似signal()
的逻辑,会将等待队列内的线程按顺序全部释放,并按照顺序加入到阻塞队列。
补充说明:
0、通过源码可以发现:
1、ReentrantLock
的newCondition()
方法,实际上是调用了内部类Sync
对象的newCondition()
方法。
2、而Sync
类继承了AbstractQueuedSynchronizer类。
3、AbstractQueuedSynchronizer内部有一个阻塞队列,所以Sync
内部也继承了这个阻塞队列(就是Condition
唤醒线程所去到的阻塞队列)。
4、所以由Sync
创建的Condition
与阻塞队列属于多对一的关系。即Sync
可以创建多个Condition
,但Sync
只有一个阻塞队列。
5、而调用Sync
对象的Lock
对象,与阻塞队列属于一对一的关系。因为一个Lock
对象,内部只有一个Sync
对象。
6、所以不同的Lock
对象拥有各自的阻塞队列。
综上所述:
- 阻塞队列是跟
lock
关联,一个lock
对应一个阻塞队列。 - 等待队列跟
condition
关联,一个condition
对应一个等待队列。 - 线程是通过
condition.await()
,与condition
和lock
关联。 - 同一个
lock
下的condition
,所有的signal/signalAll
操作,都会按照先后顺序对线程进行唤醒执行。 - 不同
lock
下的condition
,其signal/signalAll
操作,存在于不同的阻塞队列,各阻塞队列之间存在互相争夺资源的情况,因而其唤醒后的线程执行,是无序的。
举个例子,加深印象:
/**
* 验证同个阻塞队列、不同阻塞队列,线程的唤醒顺序。
* 验证condition可以对对应线程进行等待、唤醒操作。
*/
public class TestThreadCondition {
private ReentrantLock lock = new ReentrantLock();
private ReentrantLock lock3 = new ReentrantLock();
public Condition c1 = lock.newCondition();//与c2在同一个阻塞队列
public Condition c2 = lock.newCondition();//在c1在同一个阻塞队列
public Condition c3 = lock3.newCondition();//在另外的阻塞队列
//调用c1的等待
public void await1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "--await1 start ");
c1.await();
System.out.println(Thread.currentThread().getName() + "--await1 end ");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
//调用c2的等待
public void await2() {
lock.lock();
try { System.out.println(Thread.currentThread().getName() + "--await2 start ");
c2.await();
System.out.println(Thread.currentThread().getName() + "--await2 end ");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
//调用c3的等待
public void await3() {
lock3.lock();
try {
System.out.println(Thread.currentThread().getName() + "--await3 start ");
c3.await();
System.out.println(Thread.currentThread().getName() + "--await3 end ");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock3.unlock();
}
}
//唤醒所有c1的线程
public void signalAll1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "--signalAll1 ");
c1.signalAll();
}finally {
lock.unlock();
}
}
//唤醒所有c2的线程
public void signalAll2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "--signalAll2 ");
c2.signalAll();
}finally {
lock.unlock();
}
}
//唤醒所有c3的线程
public void signalAll3() {
lock3.lock();
try {
System.out.println(Thread.currentThread().getName() + "--signalAll3 ");
c3.signalAll();
}finally {
lock3.unlock();
}
}
/**
* 1、针对c1、c2、c3先各自创建5个线程
* 2、各个线程内部调用await()方法
* 3、执行创建的线程
* 4、分别调用signalAll,唤醒3个condition的所有线程
*/
public static void main(String[] args) {
TestThreadCondition t = new TestThreadCondition();
int length = 5;
Thread[] thread1s = new Thread[length];//绑定condition1
Thread[] thread2s = new Thread[length];//绑定condition2
Thread[] thread3s = new Thread[length];//绑定condition3
for(int i = 0; i < length; i++) {
Thread thread1 = new Thread(new Runnable() {
public void run() {
t.await1();
}
});
thread1.setName("Thread1-"+i);
thread1s[i] = thread1;
Thread thread2 = new Thread(new Runnable() {
public void run() {
t.await2();
}
});
thread2.setName("Thread2-"+i);
thread2s[i] = thread2;
Thread thread3 = new Thread(new Runnable() {
public void run() {
t.await3();
}
});
thread3.setName("Thread3-"+i);
thread3s[i] = thread3;
}
for(int i = 0; i < length; i++) {
thread1s[i].start();
thread2s[i].start();
thread3s[i].start();
}
try {
Thread.sleep(1000);
//唤醒
ReentrantLock l = new ReentrantLock();
l.lock();
try {
t.signalAll1();
t.signalAll2();
t.signalAll3();
}finally {
l.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
Thread1-0--await1 start
Thread3-0--await3 start
Thread3-2--await3 start
Thread2-2--await2 start
Thread3-3--await3 start
Thread1-2--await1 start
Thread2-3--await2 start
Thread3-1--await3 start
Thread1-4--await1 start
Thread2-1--await2 start
Thread2-0--await2 start
Thread1-1--await1 start
Thread3-4--await3 start
Thread1-3--await1 start
Thread2-4--await2 start
main--signalAll1
main--signalAll2
main--signalAll3
Thread1-0--await1 end
Thread3-0--await3 end
Thread3-2--await3 end
Thread3-3--await3 end
Thread3-1--await3 end
Thread1-2--await1 end
Thread1-4--await1 end
Thread1-1--await1 end
Thread1-3--await1 end
Thread2-2--await2 end
Thread3-4--await3 end
Thread2-3--await2 end
Thread2-1--await2 end
Thread2-0--await2 end
Thread2-4--await2 end
执行解析:
1、从执行结果可以看出,执行完3个signalAll操作之后,所有线程被唤醒;
2、被唤醒的线程中,先忽略Thread3线程,可以发现,Thread1线程先全部被唤醒后,才轮到Thread2线程被唤醒;
3、2证明了,Thread1和Thread2是在同一个阻塞队列中的。
由于Thread1先调用signalAll操作,所以Thread1线程先进入阻塞队列,Thread2线程后进入。
而阻塞队列按顺序执行,最终结果就如上所示,Thread1线程先全部执行完,才轮到Thread2;
4、回来看Thread3线程。Thread3线程夹杂在Thread1、2线程之间,看似无序地执行;
5、由4可以看出,Thread3线程的执行与Thread1、2没有任何联系。
因为Thread3是由lock3的condition唤醒,而Thread1、2是由lock的condition唤醒。
说明,不同lock的阻塞队列不同,所以不同lock下的唤醒是无序的,会互相抢占资源;
6、再来分别观察Thread1、2、3里面的所有线程。
可以发现,各个Threadx下面的线程,都是按照其等待(awaitx start)的顺序,被唤醒(awaitx end)的。
说明,condition调用await进入等待队列是有序的,调用signal唤醒线程也是有序的,且都是按照先进先出(FIFO)的顺序。
7、从各个Threadx线程调用的awaitx/signalAllx方法,可以发现,各个condition的await与其signal/signalAll是对应的。
说明,同一个condition调用的signal/signalAll,只能唤醒调用了自己的await的线程,影响不了其他condition的线程。
这也证明了,condition可以对指定线程进行等待唤醒操作。
6、volatile
关键字
当 JVM 设置为-server
模式的时候,线程在内部定义了私有变量,这个私有变量就会存在于公共内存和该线程的私有内存中。
而-server
模式下的 JVM 为了提高线程执行效率,会让线程一直读取其私有内存数据。这就导致了公共内存和该线程的私有内存中变量的值不同步:
1、在该线程中读取,则读取的是该线程的私有内存中的变量;
2、在该线程外读取,则读取的是公共内存中的变量。
如图:
为了保证这种情况下的数据同步,可以使用volatile
关键字。
使用volatile
,直接声明在属性上即可。
如:
public volatile int i = 1;
这样,就可以使得线程内、外获取到的私有变量是同步的了。
之所以会将volatile
放在这章介绍,是因为volatile
关键字也可以当成是一种锁的方式。因为它可以保证在所有地方调用元素的时候,都能得到最新的值,保证了值的同步。是一种类似的读锁。
但是volatile
又不是传统意义上的锁,因为它不会对元素加锁,不会对线程阻塞。相比synchronized
,volatile
是更加轻量级的同步机制。
注:synchronized
具有volatile
功能,在synchronized
内操作的变量,可以将线程内存和公共内存同步。
加了volatile
关键字后,私有变量的读取就变成了强制从公共内存中读取。
如图:
但是,就如上所说,volatile
只是保证元素在任何地方读取的时候都是最新的。所以它并不能保证操作元素的时候是同步的。
即便是如i++
这样的操作。
因为i++
也就是i=i+1
,可以具体拆分为以下三个步骤:
1、获取i
的值。 这里加了volatile
关键字,所以可以保证取到的值就是最新的;
2、计算i+1
。 这个地方是没有加锁的,所以如果此刻其他线程也在计算i
的值,这里的i+1
中的i
可能就不是上一步获取到的i
了;
3、将计算后的i
写入内存。 有可能第2步的时候计算出来的i
值就已经不是最新的了,也有可能计算出来i
值之后,又有其他线程在修改i
值,这时写入内存的i
就会造成其他线程修改后的i
变成了脏数据。或者可能在它写入内存后,其他线程又马上也把他们修改后的i
也写入了内存,导致i++
这个计算的结果没有保存到。
综上所述,volatile
只能保证读取到的是最新的数据,并不能保证对数据的操作是一个整体不被其他线程异步影响。也就是说,volatile
是不具备原子性的。
7、原子操作
就如上面说到的,volatile
不具备原子性。那么什么是原子性呢?
原子性就是使操作作为一个不可分割的整体,就像一个原子,不能再被异步拆分。类似加了synchronized
锁一样的效果。
那么要使类似i++
这样的操作保持原子性,要怎么做呢?答案就是atomic
原子类。
它类似加了synchronized
关键字的计算,每个原子操作都是不可分割,线程安全、同步的。但它又不像synchronized
,因为它并没有加锁。
常用的原子类有如下几种:
AtomicInteger
:操作int类型AtomicLong
:操作long类型AtomicBoolean
:操作boolean类型- 等等
常用方法参考如下:
getAndIncrement()
:先获取到了值,再默默加1,如i++
incrementAndGet()
:先加1,再获取加完的结果值,如++i
addAndGet(int delta)
:加具体值,再获取结果,如i=i+3
- 等等
例子:
AtomicInteger i = new AtomicInteger(100);
int val = i.getAndIncrement();
System.out.println(val); // 100
System.out.println(i.intValue()); // 101
val = i.addAndGet(9);
System.out.println(val); // 110
8、查询锁信息的相关方法
lock.getHoldCount()
:查询当前线程调用这个锁的lock()
方法的次数;lock.getQueueLength()
:返回正在等待这个锁的线程的估计数;lock.getWaitQueueLength(Condition condition)
:返回等待与这个锁相关的condition
(调用了await()
)的线程估计数;lock.hasQueuedThread(Thread thread)
:查询指定的线程thread
是否正在等待获取这个锁;lock.hasQueuedThreads()
:查询是否有线程正在等待获取这个锁;lock.hasWaiters(Condition condition)
:查询是否有线程调用了这个condition
的await()
方法,正在等待中;lock.isFair()
:判断这个锁是不是公平锁;lock.isHeldByCurrentThread()
:查看当前线程是否正持有这个锁;lock.isLocked()
:查看这个锁是否有被线程占用着;lock.lockInterruptibly()
:如果当前线程未被中断,则获取锁,如果已经中断了,则抛出异常;condition.awaitUninterruptibly()
:与await()
类似,但是调用此方法之后,再调用interrupt()中断
不会报错。调用await()
之后调用interrupt()中断
会报错。condition.awaitUntil(Date deadline)
:与await()
类似,等待deadline
时间后,自动唤醒。且在这期间,也可以被signal/signalAll
唤醒。