五、线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,Java提供了一些机制来保证线程协调运行。
1.传统的线程通信
1.1 wait()、notify()、notifyAll()
题目:要求线程1,2交替地打印1~100的数。
为了实现这个要求,可以借助于Object类提供的wait()、notify()和notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类。
三个方法的说明:
- wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该方法有三种形式——无时间参数的wait(一直等待,直到其他线程通知)、带毫秒参数的wait和带毫秒、毫微秒参数的wait(这两种方式都是在等待指定时间后自动苏醒),调用wait()方法的线程会自动释放对该同步监视器的锁定。
- notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中优先级最高的一个线程,选择是任意性的,只有当前线程放弃对该同步监视器的锁定后(使用wait方法),才可以执行被唤醒的线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
class Window implements Runnable {private int number = 1;@Overridepublic void run() {while (true) {synchronized (this) {notify();//唤醒一个其他的被阻塞线程,省略了thisif(number <= 100){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;try {this.wait();//调用该方法的线程被阻塞,同时释放锁} catch (InterruptedException e) {e.printStackTrace();}}}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}}
1.2 使用三个方法的注意事项
这三个方法必须由同步监视器对象来调用,这可分为以下两种情况:
对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
对于使用synchronized修饰的同步代码块,同步监视器是synchronized括号后的对象,必须使用该对象或同步监视器调用这三个方法,如果同步监视器和方法调用者不一致,会出现IllegalMonitorStateException异常。
class Window implements Runnable {private int number = 1;Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {notify();//调用者默认为this,同步监视器和方法调用者不一致,异常if(number <= 100){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;try {obj.wait();//使用同步监视器调用该方法} catch (InterruptedException e) {e.printStackTrace();}}}}}}//结果:IllegalMonitorStateException异常
1.3 经典面试题
sleep()和wait()方法的异同?
相同点:一旦执行这两个方法,都可以使线程进入阻塞状态。
- 不同点:
(1)sleep()在Thread类中声明,wait()在Object类中声明。
(2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()方法必须由同步监视器调用,即必须在同步代码块或同步方法中使用。
(3)如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,而wait()会释放锁。
2.使用Condition
当不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()和notifyAll()方法进行线程通信了。
当使用Lock对象进行同步时,Java提供了Condition类来保持协调,Condition实例被绑定在一个Lock对象上,要获得特定Lock对象的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法:
- await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await()方法有更多变体,可以完成更丰富的等待操作。
- signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程,选择是任意性的,只有当前线程放弃对该Lock对象的锁定后(使用await方法),才可以执行被唤醒的线程。
signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
class Window implements Runnable {private int number = 1;ReentrantLock lock = new ReentrantLock();//显式定义锁对象Condition cond = lock.newCondition();//获得指定lock对象对应的Condition@Overridepublic void run() {while (true) {lock.lock();//加锁try {cond.signal();//唤醒一个线程if(number <= 100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;try {cond.await();//阻塞当前线程} catch (InterruptedException e) {e.printStackTrace();}}else{break;}} finally {lock.unlock();//释放锁}}}}public class Test {public static void main(String[] args){Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}}
3.使用阻塞队列(BlockingQueue)
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生存者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。BlockingQueue提供如下两个支持阻塞的方法:put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
- take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
- 在队列尾部插入元素。包括add(E e)、offer(E e)和put(E e)方法,当队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
- 在队列头部删除并返回删除的元素。包括remove()、poll()和take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
六、线程池(创建线程的方式四)
1.线程池简介
系统启动一个新线程的成本是比较高的,在这种情况下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生命周期很短暂的线程时,更应该考虑使用线程池。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量。2.Executors工厂类
JAVA5提供了一个Executors工厂类来创建连接池,该工厂类包含如下几个静态工厂方法来创建连接池:
newCachedThreadPool():创建一个具有缓冲功能的线程池,系统根据需要创建线程,这些线程会被缓存在线程池中。
- new
3.创建步骤
class Thread1 implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {if (i % 2 == 0){System.out.println(Thread.currentThread().getName() + ":" + i);}}}}class Thread2 implements Callable{@Overridepublic Object call() throws Exception {for (int i = 0; i < 10; i++) {if (i % 2 != 0){System.out.println(Thread.currentThread().getName() + ":" + i);}}return null;}}public class Test {public static void main(String[] args){、//1.提供指定线程数量的线程池ExecutorService service = Executors.newFixedThreadPool(10);//2.执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象service.execute(new Thread1());//execute适用于实现Runnable接口对象service.submit(new Thread2());//submit适用于实现Callable接口对象//3.关闭连接池service.shutdown();}}
七、其他
