五、线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,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():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

    1. class Window implements Runnable {
    2. private int number = 1;
    3. @Override
    4. public void run() {
    5. while (true) {
    6. synchronized (this) {
    7. notify();//唤醒一个其他的被阻塞线程,省略了this
    8. if(number <= 100){
    9. try {
    10. Thread.sleep(10);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. System.out.println(Thread.currentThread().getName() + ":" + number);
    15. number++;
    16. try {
    17. this.wait();//调用该方法的线程被阻塞,同时释放锁
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. }
    23. }
    24. }
    25. }
    26. public class Test {
    27. public static void main(String[] args){
    28. Window w = new Window();
    29. Thread t1 = new Thread(w);
    30. Thread t2 = new Thread(w);
    31. t1.setName("线程1");
    32. t2.setName("线程2");
    33. t1.start();
    34. t2.start();
    35. }
    36. }

    1.2 使用三个方法的注意事项

    这三个方法必须由同步监视器对象来调用,这可分为以下两种情况:

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。

  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized括号后的对象,必须使用该对象或同步监视器调用这三个方法,如果同步监视器和方法调用者不一致,会出现IllegalMonitorStateException异常。

    1. class Window implements Runnable {
    2. private int number = 1;
    3. Object obj = new Object();
    4. @Override
    5. public void run() {
    6. while (true) {
    7. synchronized (obj) {
    8. notify();//调用者默认为this,同步监视器和方法调用者不一致,异常
    9. if(number <= 100){
    10. try {
    11. Thread.sleep(10);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. System.out.println(Thread.currentThread().getName() + ":" + number);
    16. number++;
    17. try {
    18. obj.wait();//使用同步监视器调用该方法
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }
    24. }
    25. }
    26. }
    27. //结果: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对象的锁定后,才可以执行被唤醒的线程。

    1. class Window implements Runnable {
    2. private int number = 1;
    3. ReentrantLock lock = new ReentrantLock();//显式定义锁对象
    4. Condition cond = lock.newCondition();//获得指定lock对象对应的Condition
    5. @Override
    6. public void run() {
    7. while (true) {
    8. lock.lock();//加锁
    9. try {
    10. cond.signal();//唤醒一个线程
    11. if(number <= 100){
    12. try {
    13. Thread.sleep(100);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. System.out.println(Thread.currentThread().getName() + ":" + number);
    18. number++;
    19. try {
    20. cond.await();//阻塞当前线程
    21. } catch (InterruptedException e) {
    22. e.printStackTrace();
    23. }
    24. }else{
    25. break;
    26. }
    27. } finally {
    28. lock.unlock();//释放锁
    29. }
    30. }
    31. }
    32. }
    33. public class Test {
    34. public static void main(String[] args){
    35. Window w = new Window();
    36. Thread t1 = new Thread(w);
    37. Thread t2 = new Thread(w);
    38. t1.setName("线程1");
    39. t2.setName("线程2");
    40. t1.start();
    41. t2.start();
    42. }
    43. }

    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.创建步骤

    1. class Thread1 implements Runnable {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 10; i++) {
    5. if (i % 2 == 0){
    6. System.out.println(Thread.currentThread().getName() + ":" + i);
    7. }
    8. }
    9. }
    10. }
    11. class Thread2 implements Callable{
    12. @Override
    13. public Object call() throws Exception {
    14. for (int i = 0; i < 10; i++) {
    15. if (i % 2 != 0){
    16. System.out.println(Thread.currentThread().getName() + ":" + i);
    17. }
    18. }
    19. return null;
    20. }
    21. }
    22. public class Test {
    23. public static void main(String[] args){、
    24. //1.提供指定线程数量的线程池
    25. ExecutorService service = Executors.newFixedThreadPool(10);
    26. //2.执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象
    27. service.execute(new Thread1());//execute适用于实现Runnable接口对象
    28. service.submit(new Thread2());//submit适用于实现Callable接口对象
    29. //3.关闭连接池
    30. service.shutdown();
    31. }
    32. }

    七、其他