线程和线程之间不是独立的个体,它们彼此之间可以互相通信和协作。
线程通信就是在线程之间传递信息,保证他们能够协同工作。在线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务的处理过程进行有效的把控与监督。
1.等待/通知机制
(1)不使用等待/通知实现线程间通信
如果不使用线程间的通信机制,两个线程想要根据同一个数据实现通信的话,就必须使用while( )轮询的方式获取共享数据的值。
比如:两个线程A和B,共享同一个int型数据i,i的初始值是0,线程A对i执行循环加一的操作,线程B在i等于5的时候执行一些操作,那么线程B怎么才能获取到i的值呢?
线程B就必须不停的通过while( )循环语句轮询的来检测i的值。这样虽然两个线程间实现了通信,但是会浪费CPU资源。
如果轮询的时间间隔很小,更浪费CPU资源;如果轮询的时间间隔很大,有可能会取不到想要得到的数据。就需要有一种机制来减少CPU的资源浪费,而且还可以实现在多个线程间通信,满足这些条件的就是“wait/notify”机制。
(2)等待/通知机制
等待/通知机制中将线程分为两种:等待线程和通知线程,等待线程的执行需要依靠对应的通知线程把相应的方法执行完毕,返回对应值给等待线程时等待线程才能执行,以此进行线程之间的通信和合作,在这个机制中一个线程的执行需要依靠其他线程。
等待/通知机制使用wait( )/notify( )方法实现。wait( )和notify( )都是Object类的方法,在调用wait( )和notify( )方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法和同步块中调用wait( )和notify( )方法,
wait( )是使当前执行代码的进程等待,wait( )方法是Object类的方法,该方法用来将当前线程置入“预执行队列”,并在wait( )所在的代码行停止执行,直到接到通知或被中断为止。在执行wait( )方法后,当前线程释放锁。
notify( )方法用来通知那些可能等待该对象对应的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。notify( )方法仅通知一个线程。
notifyAll( )方法可以使所有在阻塞状态等待某个对象锁的线程从阻塞状态退出,进入就绪状态。但这些线程有时候是随机执行,有时候就是优先级最高的那个线程最先执行,取决于JVM虚拟机的实现。
需要说明的是,在执行notify( )方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify( )方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才能释放锁,而呈wait状态所在的线程才可以获取该对象锁。
用一句话来总结wait和notify:wait使线程停止运行,而notify使停止的线程继续运行。
调用wait( )方法的线程释放对象锁,然后从运行状态退出,进入阻塞状态;调用notify( )方法唤醒因调用wait( )方法而处于阻塞状态的线程,进入就绪状态。如果发出notify操作时没有处于阻塞状态中的线程,就是没有调用wait( )方法的线程,那么该命令会被忽略。
wait( )方法和notify( )方法一起使用,他们释放和获得的对象锁也要对应。一个持有对象锁的notify( )方法只能释放执行wait( )方法等待该对象锁的线程,而不能释放执行wait( )方法等待其他对象锁的线程。
在之前有介绍过和线程状态有关的方法,这些方法可以改变线程对象的状态,线程状态和他们之间的转换关系和对应的线程状态转换方法如图:
在这张图中,运行包括运行态和就绪态, 暂停就是阻塞态。
每个锁对象都有两个队列,一个就绪队列,一个阻塞队列。就绪队列存储了将要获得对象锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度;反之,一个线程被wait( )后,就会进入阻塞队列,等待下一次被唤醒。
(3)方法wait锁释放与notify锁不释放
当方法wait( )执行后,锁被自动释放。但执行完notify( )方法,锁却不自动释放,必须执行完notify( )方法所在的同步synchronized代码块后才释放锁。
(4)当线程呈wait状态时,调用对象线程的interrupt( )方法会出现InterruptedException异常。
在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
(5)如果有多个线程执行wait( )方法,调用notify( )方法一次只随机通知一个线程进行唤醒。
(6)调用notifyAll( )方法唤醒所有线程。
如果有多个线程执行wait( )方法,可以多次使用notify( )方法唤醒这些线程,但是可能存在有的线程没被唤醒,要想唤醒所有的线程,调用notifyAll( )方法唤醒所有等待的线程。
(7)使用wait(long)方法自动唤醒线程
带一个参数wait(long)方法的功能是等待指定的时间,在某一时间内看是否有线程对锁进行唤醒,如果超过这个时间还没有被唤醒,则不需要使用notity( )方法来唤醒而是自动唤醒。也可以由notify( )方法唤醒,如果执行notify( )方法唤醒执行wait(long)方法的线程执行的时间比执行wait(long)方法的线程自动唤醒本身花的时间要短,则执行wait(long)方法的线程就不用等待参数中规定的那么长时间而是notify( )方法执行完之后就可以被唤醒。
(8)生产者/消费者模式实现
为了更好的解决多个交互线程之间的运行速度匹配问题(即同步问题),Java引入了多线程同步模型的概念。等待/通知模式是多线程同步模型的一种。等待/通知模式最经典的案例就是“生产者/消费者模式”,原理基于wait/notify。
当一个生产者和一个消费者共享一个对象锁的时候他们能很好的交替运行。
但是当出现一个生产者多个消费者或多个生产者多个消费者的情况时,就会可能会出现程序的“假死”,“假死”就是死锁。程序“假死”现象就是所有的线程都处于WAITING的状态,程序的执行无法向前推进的一种状态。
出现这种情况的原因是多个生产者和多个消费者共享一个对象锁,那么当有线程调用notify( )方法唤醒一个线程时,唤醒的线程不仅有可能是异类,还有可能是同类,比如生产者调用notify( )方法不仅可以唤醒等待中的消费者,还可以唤醒生产者,消费者调用notify( )方法不仅可以唤醒等待中的生产者,还可以唤醒消费者,因为他们持有的对象锁都是一样的。如果唤醒同类的情况越来越多,就会有越来越多的同类被唤醒后又被阻塞,异类也没有被唤醒的机会,结果就是导致被阻塞的线程越来越多,最终可能导致所有的线程都处于等待状态而造成“假死”。
解决的方法就是在每个线程释放正在等待该对象锁的其他线程时使用notifyAll( )方法取代notify( )方法,这样每次都能唤醒所有线程而不会出现阻塞。
(9)通过管道进行线程间通信:字节流
在Java语言中提供了各种各样的输入/输出流Stream,可以使我们很方便的能够对数据进行操作,其中管道流是一种特殊的流,用于在不同线程之间直接传递数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信。
在Java的JDK中提供了4个类来使线程间可以进行通信:
<1>PipedInputStream和PipedOutputStream,这是基于字节流。
<2>PipedReader和PipedWriter,这是基于字符流。
(10)通过管道进行线程间通信:字符流
在管道中可以传递字节流,也可以传递字符流。可以在两个线程中通过管道流进行字符数据的传输。
(11)方法join( )的使用
方法join( )在和线程状态有关的方法有讲过它的基本使用。它可以使调用该方法的线程先执行。一个可以使用的场景就是主线程想等待子线程执行完之后再执行,比如子线程处理一个数据,主线程要取得这个数据中的值,就可以使用join( )方法。join( )方法的作用是当前线程等待调用该方法的线程结束后,再排队等待CPU资源。
方法join( )具有使线程排队运行的效果,有些类似同步的运行效果。join和synchronized的区别是:join在内部使用wait( )方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。
方法join(long) 和sleep(long)的区别:
方法join(long)的功能是在内部使用wait(long)方法实现的,所以join(long)具有释放锁的特点。
在执行wait(long)方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。而Thread.sleep(long)方法却具有不释放锁的特点。
参考:《Java多线程编程核心技术》 --- 高洪岩(Chapter3 3.1 3.2)