一.等待和通知机制
等待和通知机制其实就是同步机制,帮助线程之间通信,让一个线程通知另外一个线程某种特定的条件发生了。java 多线程中的等待唤醒,有两种实现方法
①通过wait和notify,notifyAll方法来配合完成的
②通过线程锁(ReentrantLock)、线程通信状态(Condition)
二.synchronized、wait和notify
synchronized
用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,它获得了对象锁, 其它线程对该对象所有同步代码部分的访问都被暂时阻塞。
void wait()
等待某种条件的发生。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。
void wait(long timeout)
等待一个条件的发生。但是,如果在设定的时间内没有收到通知,就返回。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。
void notify()
通知线程其等待的条件已经发生了。这是Object类的方法,而且必须在被同步的方法或者代码块中被调用。
wait() / notify() 都是Object类的方法。每一个Java对象都是直接或间接继承Object类,所以任何Java对象都支持这种机制。所以等待和通知机制存在于Java系统中每一个对象里。
等待和通知机制并不指定该条件到底是什么。事实上,一般情况下也不需要指定。程序员决定一个线程在执行到代码的某处时在某个对象上调用wait()方法进行等待,另一个线程执行一段代码后认为条件满足了,则在同一个对象上调用notify()方法,通知等待该对象的线程。
当我们使用wait(),notify(),notifyAll()方法时,很容易出现java.lang.IllegalMonitorStateException异常。这个异常的原因就是当前线程没有获得对象的锁,或者说,当前调用的对象不是锁对象。所以我们必须明白调用wait(),notify(),notifyAll()的对象必须是当前锁对象。实例程序如下:
import java.util.Random; public class SynchronizedTest implements Runnable { @Override |
public class SynchronizedMainTest { public static void main(String[] args) { |
其运行结果如下:
Thread-0 get into setName().
Thread-1 get into setName().
Thread-1 set success,the name is reset to:宜一一22
Thread-0 set success,the name is reset to:宜一一5
Student类作为一个资源类属性如下, getter和setter方法省略 :
public class Student {
private String name;
private String stuId;
private String grade;
private int age;
可以看到,在 setName() 方法里面,首先打印 System.out.println(Thread.currentThread().getName() + " get into setName()."); 之后修改 SynchronizedTest 对象的标志位flag,进入wait状态,代码如下:
if(flag == true){
flag = false;
this.wait();
}
与Thread.sleep()方法不同的是,调用this.wait()将放弃该线程持有的对象锁,其他在等待该对象锁的线程可以进入到该同步方法内,此时标志位已经修改,所以其他线程可以继续向下执行,当线程执行到 最后已经正确修改Student对象的名字时,调用了 notify()方法来通知其他还在等待该对象锁的线程对象,也就是一开始就放弃对象锁的那个线程。之前的线程被唤醒后继续之前没有做完的工作,修改Student对象的名字后退出同步方法区。
值得一提的是在使用wait()和notify()方法时,必须是在获得该对象的对象锁的时候使用,之前有将this.wait()修改为Thread.currentThread().wait()时,程序输出结果如下:
Thread-0 get into setName(). Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:503) at com.thread.locktest.SynchronizedTest.setName(SynchronizedTest.java:19) at com.thread.locktest.SynchronizedTest.run(SynchronizedTest.java:52) at java.lang.Thread.run(Thread.java:745) Thread-1 get into setName(). Thread-1 set success,the name is reset to:宜一一44 |
所以在使用wait()和notify()方法时,一定要清楚当前线程持有的是哪一个对象的对象锁。在 setAge() 里获得的是Student对象锁,所以初始化 SynchronizedTest 对象的时候,如果传入的是同一个Student对象时,不同的线程对象进入 setAge() 方法也必须等待Student的对象锁。
三.线程锁(ReentrantLock)
多线程并发索取某一资源,要求该资源线程安全(即线程同步),每一线程在使用资源时候会需要检查资源状态,如果状态不符合立即通知资源的维护服务进行维护,维护完毕后发布通知表示该资源可以继续被索取使用
所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作。
3.1什么是reentrantlock
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多 线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
reentrant 锁意味着什么呢?简 单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
3.2 ReentrantLock与synchronized的比较
相同:ReentrantLock提供了synchronized类似的功能和内存语义。
不同:
(1)ReentrantLock 功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合 适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个 Condition实例,所以更有扩展性。
(2)ReentrantLock 的性能比synchronized会好点。
(3)ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。
3.3 ReentrantLock扩展的功能
3.3.1 实现可轮询的锁请求
在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误恢复机制,可以规避死锁的发生。
如果你不能获得所有需要的锁,那么使用可轮询的获取方式 使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状 态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下:
- Lock lock = ...;
- if (lock.tryLock()) {
- try {
- // manipulate protected state
- } finally {
- lock.unlock();
- }
- } else {
- // perform alternative actions
- }
3.3.2 实现可定时的锁请求
当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活
动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。
3.3.3 实现可中断的锁获取请求
可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。
3.4 ReentrantLock不好与需要注意的地方
(1) lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放
(2) 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。
Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能.Lock对象的使用如下:
import java.util.concurrent.locks.Lock; public class LockTest { private static Lock lock = new ReentrantLock(); } |
运行结果如下:
Thread-0 has got the lock.
Thread-0 has done the job.
Thread-1 has got the lock.
Thread-1 has done the job.