对Java并发编程学习已经有段时间了,知识需要系统的学习才能更好的理解,也要勤于整理才不至于学了之后时间一长就模糊,然后实践中多用,该总结的时候还是不能偷懒的。
从事开发时,有时会遇到线程之间的协作问题。obj.wait(),obj.notify(),obj.notifyAll(),t.join(),t.yield()成为了我们进行线程协作的常用方法,他们的区别和原理在网上很容易就可以搜到。
本人在从事开发的过程中,数次理清他们之间的关系,但一直没有做过总结整理,时间一长记忆就模糊了。
一、Java线程状态:
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
二、Wait-Notify机制
wait,notify,notifyAll在线程同步是我们经常用到,首先要弄清楚一点,这三个方法并不是Thread类的方法,他们是object类的方法。所以Java中一切对象都有这三个方法。
以下我默认使用的是对象锁,类锁也是同样的道理。
(1)wait
wait是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以wait必须要写在synchronized代码块中,否则会报“java.lang.IllegalMonitorStateException”异常。(我们只需牢牢记住,要释放锁,必须要先获得锁。)
(2)notify,notifyAll
notify跟notifyAll,作用很相似。他们也需要写在synchronized代码块中,调用对象的这两个方法也需要先获得该对象的锁。notify,notifyAll,唤醒等待该对象同步锁的线程。notify唤醒对象等待池中的任意一个线程,将这个线程放入该对象的锁池中。对象的锁池中线程可以去竞争得到对象锁,然后开始执行。notify去唤醒线程的时候是随机的,没有优先级之分,也不会因为先到就先被唤醒。notifyAll唤醒对象等待池中的所有线程,把这些线程都加到对象的锁池中,让他们去竞争锁。notify,notifyAll调用时并不会释放对象锁。所以,如下代码:1
2
3
4
5
6
7
8
9
10public void test()
{
Object object = new Object();
synchronized (object){
object.notifyAll();
while (true){
}
}
}
虽然调用了notifyAll,但是紧接着进入了一个死循环。导致一直不能出临界区,一直不能释放对象锁。所以,即使它把所有在等待池中的线程都唤醒放到了对象的锁池中,但是锁池中的所有线程都不会运行,因为他们都拿不到锁。
对象的等待池,对象的锁池。
前面我一直提到两个概念,等待池,锁池。这两者是不一样的。
锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
如下代码:1
2
3
4
5
6
7
8
9
10
11Object object = new Object();
public void test()
{
synchronized (object){ =======A
try {
object.wait(); =======B
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程1如果卡在A处,那么他会被放入object的锁池。如果在B处调用了wait方法,会释放对象锁,被放入object的等待池。等待其他线程调用了object.notifyAll(),才会被唤醒,放入object的锁池去竞争锁。也就是说,在锁池中才有资格去竞争锁。在等待池中是没有的,只能先等待别人把它唤醒。
三、sleep和wait的区别:
1、首先,要记住这个差别,“sleep是Thread类的静态方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是有区别的。
2、Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁,调用Thread.sleep是不会影响锁的相关行为;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);
3、Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。
4、wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
四、Thread.yield()
在Thread.java中yield()定义如下:1
2
3
4
5
6
7/**
* A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore
* this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.
* Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.
*/
public static native void yield();
一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。
- yield是一个静态的原生(native)方法。
- yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
- yield不能保证使得当前正在运行的线程迅速转换到Runnable状态,
它仅能使一个线程从运行状态转到Runnable状态,而不是等待或阻塞状态。
yield方法使用示例:
在下面的示例程序中,我随意的创建了名为生产者和消费者的两个线程。生产者设定为最小优先级,消费者设定为最高优先级。在Thread.yield()注释和非注释的情况下分别运行该程序。没有调用yield()方法时,虽然输出有时改变,但是通常消费者行先打印出来,然后事生产者。
调用yield()方法时,两个线程依次打印,然后将执行机会交给对方,一直这样进行下去。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package test.core.threads;
public class YieldExample
{
public static void main(String[] args)
{
Thread producer = new Producer();
Thread consumer = new Consumer();
producer.setPriority(Thread.MIN_PRIORITY); //Min Priority
consumer.setPriority(Thread.MAX_PRIORITY); //Max Priority
producer.start();
consumer.start();
}
}
class Producer extends Thread
{
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println("I am Producer : Produced Item " + i);
Thread.yield();
}
}
}
class Consumer extends Thread
{
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println("I am Consumer : Consumed Item " + i);
Thread.yield();
}
}
}
上述程序在没有调用yield()方法情况下的输出:1
2
3
4
5
6
7
8
9
10I am Consumer : Consumed Item 0
I am Consumer : Consumed Item 1
I am Consumer : Consumed Item 2
I am Consumer : Consumed Item 3
I am Consumer : Consumed Item 4
I am Producer : Produced Item 0
I am Producer : Produced Item 1
I am Producer : Produced Item 2
I am Producer : Produced Item 3
I am Producer : Produced Item 4
上述程序在调用yield()方法情况下的输出:1
2
3
4
5
6
7
8
9
10I am Producer : Produced Item 0
I am Consumer : Consumed Item 0
I am Producer : Produced Item 1
I am Consumer : Consumed Item 1
I am Producer : Produced Item 2
I am Consumer : Consumed Item 2
I am Producer : Produced Item 3
I am Consumer : Consumed Item 3
I am Producer : Produced Item 4
I am Consumer : Consumed Item 4
五、t.join()
线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。1
2
3//Waits for this thread to die.
public final void join() throws InterruptedException
在join()方法内设定超时,使得join()方法的影响在特定超时后无效。当超时时,主方法和任务线程申请运行的时候是平等的。然而,当涉及sleep时,join()方法依靠操作系统计时,所以你不应该假定join()方法将会等待你指定的时间。
像sleep,join通过抛出InterruptedException对中断做出回应。
join()方法使用示例
1 | package test.core.threads; |