沉淀再出发:再谈java的多线程机制
一、前言
自从我们学习了操作系统之后,对于其中的线程和进程就有了非常深刻的理解,但是,我们可能在C,C++语言之中尝试过这些机制,并且做过相应的实验,但是对于java的多线程机制以及其中延伸出来的很多概念和相应的实现方式一直都是模棱两可的,虽然后来在面试的时候可能恶补了一些这方面的知识,但是也只是当时记住了,或者了解了一些,等到以后就会变得越来越淡忘了,比如线程的实现方式有两三种,线程池的概念,线程的基本生命周期等等,以及关于线程之间的多并发引起的资源的抢占和竞争,锁的出现,同步和异步,阻塞等等,这些概念再往下面延伸就到了jvm这种虚拟机的内存管理层面上了,由此又出现了jvm的生存周期,内存组成,函数调用,堆和栈,缓存,volatile共享变量等等机制,至此我们才能很好的理解多线程和并发。
二、java的多线程初探
2.1、进程和线程的生命周期
让我们看看网上对多线程生命周期的描述:
Java线程具有五中基本状态:
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,
随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
6 .同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
这种解释其实和我们在操作系统中学习的是一致的,只不过内部的实现方式有所不同而已,同样的如果实在Linux之中,进程和线程的生命周期有略微有所不同,但是究其根源来说都是这几种步骤,只不过在某种过程之下可能有所细分而已。
再比如说其他资料上对java的多线程生命周期的划分,我们也可以看到就是把其中的阻塞状态分离出来而已:
明白了这一点,对于我们继续细分其中的状态背后的意义至关重要。
2.2、多线程状态的实现
2.2.1、start()
新启一个线程执行其run()方法,一个线程只能start一次。主要是通过调用native start0()来实现。
public synchronized void start() {
//判断是否首次启动
if (threadStatus != )
throw new IllegalThreadStateException(); group.add(this); boolean started = false;
try {
//启动线程
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
2.2.2、run()
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当该线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,如果继承Thread类则必须重写run方法,在run方法中定义具体要执行的任务。start()的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!
public void run() {
if (target != null) {
target.run();
}
}
target是一个Runnable对象。run()就是直接调用Thread线程的Runnable成员的run()方法,并不会新建一个线程。
2.2.3、sleep()
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。sleep() 定义在Thread.java中。sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。
我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。
package com.thread.test; public class SleepLockTest{ private static Object obj = new Object(); public static void main(String[] args){
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
} static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run(){
// 获取obj对象的同步锁
synchronized (obj) {
try {
for(int i=0; i <10; i++){
System.out.printf("%s: %d\n", this.getName(), i);
// i能被4整除时,休眠100毫秒
if (i%4 == 0)
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
sleep不会释放同步锁
2.2.4 yield()
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
package com.thread.test; class ThreadB extends Thread {
public ThreadB(String name) {
super(name);
} public synchronized void run() {
for (int i = 0; i < 10; i++) {
System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
// i整除4时,调用yield
if (i % 4 == 0)
Thread.yield();
}
}
} public class YieldTest {
public static void main(String[] args) {
ThreadB t1 = new ThreadB("t1");
ThreadB t2 = new ThreadB("t2");
t1.start();
t2.start();
}
}
yield让步,变为就绪态,可能切换线程
可以看到这两次的让步效果是不错的。
wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.yield();但是,t2是不会获取cpu执行权的。因为t1并没有释放“obj所持有的同步锁”。
package com.thread.test; public class YieldLockTest{ private static Object obj = new Object(); public static void main(String[] args){
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
} static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run(){
// 获取obj对象的同步锁
synchronized (obj) {
for(int i=0; i <10; i++){
System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
// i整除4时,调用yield
if (i%4 == 0)
Thread.yield();
}
}
}
}
}
yield不释放同步锁
2.2.5 join()
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
join方法有三个重载版本:
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
join()实际是利用了wait(),只不过它不用等待notify()/notifyAll(),且不受其影响。它结束的条件是:1)等待时间到;2)目标线程已经run完(通过isAlive()来判断)。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = ; if (millis < ) {
throw new IllegalArgumentException("timeout value is negative");
} //0则需要一直等到目标线程run完
if (millis == ) {
while (isAlive()) {
wait();
}
} else {
//如果目标线程未run完且阻塞时间未到,那么调用线程会一直等待。
while (isAlive()) {
long delay = millis - now;
if (delay <= ) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
Join方法是通过wait实现的,当main线程调用t.join时候,main线程会获得线程对象t的锁,调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出或者时间到。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
package com.thread.test; import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep; /**
* Created with IntelliJ IDEA.
* User: Blank
* Date: 14-3-28
* Time: 下午7:49
*/
public class JoinTest implements Runnable { public static void main(String[] sure) throws InterruptedException {
Thread t = new Thread(new JoinTest());
long start = System.currentTimeMillis();
t.start();
t.join(1000);//等待线程t 1000毫秒
System.out.println(System.currentTimeMillis()-start);//打印出时间间隔
System.out.println("Main finished");//打印主线程结束
} @Override
public void run() {
// synchronized (currentThread()) {
for (int i = 1; i <= 5; i++) {
try {
sleep(1000);//睡眠5秒,循环是为了方便输出信息
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠" + i);
}
System.out.println("TestJoin finished");//t线程结束
}
//}
}
主线程得到锁之后先执行完
package com.thread.test; import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep; /**
* Created with IntelliJ IDEA.
* User: Blank
* Date: 14-3-28
* Time: 下午7:49
*/
public class JoinTest implements Runnable { public static void main(String[] sure) throws InterruptedException {
Thread t = new Thread(new JoinTest());
long start = System.currentTimeMillis();
t.start();
t.join(1000);//等待线程t 1000毫秒
System.out.println(System.currentTimeMillis()-start);//打印出时间间隔
System.out.println("Main finished");//打印主线程结束
} @Override
public void run() {
synchronized (currentThread()) {
for (int i = 1; i <= 5; i++) {
try {
sleep(1000);//睡眠5秒,循环是为了方便输出信息
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠" + i);
}
System.out.println("TestJoin finished");//t线程结束
}
}
}
main得不到锁,最后结束
2.2.6、interrupt()
此操作会中断等待中的线程,并将线程的中断标志位置位。如果线程在运行态则不会受此影响。
可以通过以下三种方式来判断中断:
)isInterrupted()
此方法只会读取线程的中断标志位,并不会重置。
)interrupted()
此方法读取线程的中断标志位,并会重置。
)throw InterruptException
抛出该异常的同时,会重置中断标志位。
2.2.6.1、终止处于“阻塞状态”的线程
通常,我们通过“中断”方式终止处于“阻塞状态”的线程。当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:
@Override
public void run() {
try {
while (true) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}
在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理。
@Override
public void run() {
while (true) {
try {
// 执行任务...
} catch (InterruptedException ie) {
// InterruptedException在while(true)循环体内。
// 当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出
break;
}
}
}
上面的InterruptedException异常的捕获在whle(true)之内。当产生InterruptedException异常时,被catch处理之外,仍然在while(true)循环体内;要退出while(true)循环体,需要额外的执行退出while(true)的操作。
2.2.6.2、 终止处于“运行状态”的线程
通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。
通过“中断标记”终止线程:
@Override
public void run() {
while (!isInterrupted()) {
// 执行任务...
}
}
isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。注意interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。
通过“额外添加标记”终止处于“运行状态”的线程,线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。注意将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
private volatile boolean flag= true;
protected void stopTask() {
flag = false;
}
@Override
public void run() {
while (flag) {
// 执行任务...
}
}
综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:
@Override
public void run() {
try {
// 1. isInterrupted()保证,只要中断标记为true就终止线程。
while (!isInterrupted()) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
}
}
正常中断并退出的案例:
package com.thread.test; class MyThread extends Thread { public MyThread(String name) {
super(name);
} @Override
public void run() {
try {
int i=0;
while (!isInterrupted()) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
}
} public class Test1 { public static void main(String[] args) {
try {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new."); t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started."); // 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); // 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
中断结束线程
中断之后死循环的案例:
package com.thread.test; class MyThread1 extends Thread { public MyThread1(String name) {
super(name);
} @Override
public void run() {
int i=0;
while (!isInterrupted()) {
try {
Thread.sleep(100); // 休眠100ms
} catch (InterruptedException ie) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
}
} public class Test2 { public static void main(String[] args) {
try {
Thread t1 = new MyThread1("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new."); t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started."); // 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); // 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
死循环
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (RUNNABLE) loop 3
t1 (RUNNABLE) loop 4
t1 (RUNNABLE) loop 5
t1 (TIMED_WAITING) is interrupted now.
t1 (RUNNABLE) loop 6
t1 (RUNNABLE) loop 7
t1 (RUNNABLE) loop 8
t1 (RUNNABLE) loop 9
t1 (RUNNABLE) loop 10
t1 (RUNNABLE) loop 11
t1 (RUNNABLE) loop 12
t1 (RUNNABLE) loop 13
t1 (RUNNABLE) loop 14
t1 (RUNNABLE) loop 15
t1 (RUNNABLE) loop 16
t1 (RUNNABLE) loop 17
t1 (RUNNABLE) loop 18
t1 (RUNNABLE) loop 19
t1 (RUNNABLE) loop 20
t1 (RUNNABLE) loop 21
t1 (RUNNABLE) loop 22
t1 (RUNNABLE) loop 23
t1 (RUNNABLE) loop 24
t1 (RUNNABLE) loop 25
t1 (RUNNABLE) loop 26
t1 (RUNNABLE) loop 27
t1 (RUNNABLE) loop 28
t1 (RUNNABLE) loop 29
t1 (RUNNABLE) loop 30
t1 (RUNNABLE) loop 31
t1 (RUNNABLE) loop 32
t1 (RUNNABLE) loop 33
t1 (RUNNABLE) loop 34
t1 (RUNNABLE) loop 35
t1 (RUNNABLE) loop 36
。。。。。。
程序进入了死循环,这是因为t1在“等待(阻塞)状态”时,被interrupt()中断;此时,会清除中断标记[即isInterrupted()会返回false],而且会抛出InterruptedException异常(该异常在while循环体内被捕获)。因此,t1理所当然的会进入死循环了。解决该问题,需要我们在捕获异常时,额外的进行退出while循环的处理。例如,在MyThread的catch(InterruptedException)中添加break 或 return就能解决该问题。
解决方案:
package com.thread.test; class MyThread3 extends Thread { private volatile boolean flag= true;
public void stopTask() {
flag = false;
} public MyThread3(String name) {
super(name);
} @Override
public void run() {
synchronized(this) {
try {
int i=0;
while (flag) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
} catch (InterruptedException ie) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
}
}
} public class Test3 { public static void main(String[] args) {
try {
MyThread3 t1 = new MyThread3("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new."); t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started."); // 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.stopTask();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); // 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用特殊标志
2.2.7、suspend()/resume()
挂起线程,直到被resume,才会苏醒。但调用suspend()的线程和调用resume()的线程,可能会因为争锁的问题而发生死锁,所以JDK 7开始已经不推荐使用了。Thread中的stop()和suspend()方法,由于固有的不安全性,已经建议不再使用!
2.2.8、wait(), notify(), notifyAll()
在Object.java文件中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
Object类中关于等待/唤醒的API详细信息如下:
wait() -- 让当前线程处于“等待(阻塞)状态”, “直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
wait(long timeout) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。当timeout为0时,表示无限等待,直到被notify()或notifyAll()唤醒
wait(long timeout, int nanos) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。处理时,由于纳秒级时间太短, 所以对参数nanos 其采取了近似处理,即大于半毫秒的加1毫秒,小于1毫秒则舍弃,其主要作用应该在能更精确控制等待时间(尤其在高并发时,毫秒的时间节省也是很值得的) notify() -- 唤醒在此对象监视器上等待的单个线程。
notifyAll() -- 唤醒在此对象监视器上等待的所有线程。
实例介绍:
package com.thread.test; class ThreadA extends Thread{ public ThreadA(String name) {
super(name);
} public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+" call notify()");
// 唤醒当前的wait线程
notify();
}
}
} public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); synchronized(t1) {
try {
// 启动“线程t1”
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start(); // 主线程等待t1通过notify()唤醒。
System.out.println(Thread.currentThread().getName()+"开始调用 wait(),导致自身释放锁,同时线程获得锁,进入synchronized");
t1.wait(); System.out.println("线程执行完,通过notify通知主线程,同时完成之后释放锁。。。");
System.out.println(Thread.currentThread().getName()+" continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
主线程调用t1.wait()让t1的线程执行
结果说明:
(01) 注意,图中"主线程" 代表“主线程main”。"线程t1" 代表WaitTest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。
(02) “主线程”通过 new ThreadA("t1") 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。
(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyAll()将其唤醒。
(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。
(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。
“当前线程”在调用wait()时,必须拥有该对象的同步锁。该线程调用wait()之后,会释放该锁;然后一直等待直到“其它线程”调用对象的同步锁的notify()或notifyAll()方法。然后,该线程继续等待直到它重新获取“该对象的同步锁”,就可以接着运行。注意:jdk的解释中,说wait()的作用是让“当前线程”等待,而“当前线程”是指正在cpu上运行的线程!这也意味着,虽然t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!
package com.thread.test; class ThreadA extends Thread{ public ThreadA(String name) {
super(name);
} public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+" call notify()");
// 唤醒当前的wait线程
notify();
}
}
} public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); synchronized(t1) {
// try {
// 启动“线程t1”
System.out.println(Thread.currentThread().getName()+" start t1");
t1.start(); // 主线程等待t1通过notify()唤醒。
System.out.println(Thread.currentThread().getName()+"开始调用 wait(),导致自身释放锁,同时线程获得锁,进入synchronized");
// t1.wait(); System.out.println("线程执行完,通过notify通知主线程,同时完成之后释放锁。。。");
System.out.println(Thread.currentThread().getName()+" continue");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
主线程不释放锁,则结束之后子线程执行run()
通过前面的示例,我们知道 notify() 可以唤醒在此对象监视器上等待的单个线程。下面,我们通过示例演示notifyAll()的用法;它的作用是唤醒在此对象监视器上等待的所有线程。
package com.thread.test; public class NotifyAllTest { private static Object obj = new Object();
public static void main(String[] args) { ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
ThreadA t3 = new ThreadA("t3");
t1.start();
t2.start();
t3.start(); try {
System.out.println(Thread.currentThread().getName()+" sleep(3000)");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} synchronized(obj) {
// 主线程等待唤醒。
System.out.println(Thread.currentThread().getName()+" notifyAll()");
obj.notifyAll();
}
} static class ThreadA extends Thread{ public ThreadA(String name){
super(name);
} public void run() {
synchronized (obj) {
try {
// 打印输出结果
System.out.println(Thread.currentThread().getName() + " wait"); // 唤醒当前的wait线程
obj.wait(); // 打印输出结果
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
notifyall的用法
(01) 主线程中新建并且启动了3个线程"t1", "t2"和"t3"。
(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设"t1", "t2"和"t3"这3个线程都运行了。
以"t1"为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityAll()来唤醒它;
相同的道理,"t2"和"t3"也会等待其它线程通过nofity()或nofityAll()来唤醒它们。
(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyAll() 唤醒obj上的等待线程,即唤醒"t1", "t2"和"t3"这3个线程。
紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,"t1", "t2"和"t3"就可以获取“obj锁”而继续运行了!
为什么notify(), wait()等函数定义在Object中,而不是Thread中?
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!线程调用wait()之后,会释放它锁持有的“同步锁”;而且我们知道:等待线程可以被notify()或notifyAll()唤醒。那么notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。
负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。
2.2.9、LockSupport的park()和unpark()
其实这个内容不是多线程的,但是也和同步,并发密切相关,因为这两种操作类似于wait()和notify(),但是比这些使用简单好用了许多。
park:阻塞当前线程(Block current thread),字面理解park,就是占住,停车的时候不就把这个车位给占住了么?起这个名字还是很形象的。
unpark: 使给定的线程停止阻塞(Unblock the given thread blocked )。
LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。LockSupport很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。
许可默认是被占用的,调用park()时获取不到许可,会进入阻塞状态。LockSupport是不可重入的,如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去。LockSupport许可的获取和释放,一般来说是对应的,如果多次unpark,只有一次park也不会出现什么问题,结果是许可处于可用状态。线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
} public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
从源码可以看出,最终是通过调用unsafe的park和unpark方法。
public native void unpark(Object obj) public native void park(boolean isAbsolute,long time)
isAbsolute参数是指明时间是绝对的(true),还是相对的(false)。
如果是相对的,time的单位是纳秒,使线程阻塞多少纳秒,如(3秒后: 3*1000*1000*1000),如果为0则是无限阻塞。
如果是绝对的,time的单位是毫秒,使线程阻塞到指定时间点, 如(2秒后:System.currentTimeMillis()+2000);
unpark相当于资源数量设置为1, 可以多次调用,但和执行一次没有区别。park相当于使用资源,把资源数量设置成0。调用时候,如果资源数为1,则不会阻塞线程,如果资源数已经为0则会阻塞线程。
park与unpark的优势:在多线程的时候,可以在不同的线程调用park或者unpark,不需要像调用wait/notify/notifyAll时,使用一个Object对象存储相应状态,如一个对象调用了wait需要调用notify唤醒,否则就会一直wait,notify只会唤醒一个线程,如果有两个线程调用同一对象的wait,则需要调用notifyAll才行。park和unpark解耦了线程之间的同步问题。
2.3、多线程的实现方式
2.3.1、继承Thread实现多线程
package com.thread.impl; class MyThread extends Thread{
private int ticket=10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(this.getName()+" 卖票:ticket"+this.ticket--);
}
}
}
}; public class ThreadTest {
public static void main(String[] args) {
// 启动3个线程t1,t2,t3;每个线程各卖10张票!
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
Thread-0 卖票:ticket10
Thread-1 卖票:ticket10
Thread-2 卖票:ticket10
Thread-1 卖票:ticket9
Thread-0 卖票:ticket9
Thread-1 卖票:ticket8
Thread-2 卖票:ticket9
Thread-2 卖票:ticket8
Thread-1 卖票:ticket7
Thread-0 卖票:ticket8
Thread-1 卖票:ticket6
Thread-2 卖票:ticket7
Thread-1 卖票:ticket5
Thread-0 卖票:ticket7
Thread-1 卖票:ticket4
Thread-2 卖票:ticket6
Thread-1 卖票:ticket3
Thread-0 卖票:ticket6
Thread-1 卖票:ticket2
Thread-2 卖票:ticket5
Thread-1 卖票:ticket1
Thread-0 卖票:ticket5
Thread-2 卖票:ticket4
Thread-0 卖票:ticket4
Thread-2 卖票:ticket3
Thread-0 卖票:ticket3
Thread-2 卖票:ticket2
Thread-0 卖票:ticket2
Thread-2 卖票:ticket1
Thread-0 卖票:ticket1
2.3.2、实现Runable接口
package com.thread.impl; //RunnableTest.java 源码
class MyThread2 implements Runnable{
private int ticket=10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票:ticket"+this.ticket--);
}
}
}
}; public class RunnableTest {
public static void main(String[] args) {
MyThread2 mt=new MyThread2(); // 启动3个线程t1,t2,t3(它们共用一个Runnable对象),这3个线程一共卖10张票!
//但是因为多线程并发的问题,在票减一之前,可能有多个线程读取了内存的值,从而造成混乱
Thread t1=new Thread(mt);
Thread t2=new Thread(mt);
Thread t3=new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
从结果中我们就能发现很多问题,因为这三个线程同时访问一块公共资源,可能造成这样的情况,同一时刻两个线程(T1,T2)都进行了读操作,获得了相同的值,一个进程(T1)输出之后,切换成第三个线程(T3),将现有的值减一,此时切换成T2,这个线程拿着原来更大的值输出之后,减一并写入内存,导致最后的数目大于10,并且有的票重复。主线程main创建并启动3个子线程,而且这3个子线程都是基于“mt这个Runnable对象”而创建的,它们共享了MyThread接口。
注意:这里之所以不是每个线程都使用10次,是因为这三个线程共用了ticket这个变量,但是每一个线程其实都是走完了run()中的for循环的。
2.3.3、使用Callable和Future接口创建线程
具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
package com.thread.impl; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; public class CallableFuture { public static void main(String[] args) { // 创建MyCallable对象
Callable<Integer> myCallable = new MyCallable();
//使用FutureTask来包装MyCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
//FutureTask对象作为Thread对象的target创建新的线程
Thread thread = new Thread(ft);
//线程进入到就绪状态
thread.start();
}
} System.out.println("主线程for循环执行完毕.."); try {
int sum = ft.get();
//取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} }
} class MyCallable implements Callable<Integer> {
private int i = 0; // 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
main 21
main 22
main 23
main 24
main 25
main 26
main 27
main 28
main 29
main 30
main 31
main 32
main 33
main 34
main 35
main 36
main 37
main 38
Thread-0 0
main 39
Thread-0 1
main 40
Thread-0 2
main 41
Thread-0 3
main 42
Thread-0 4
main 43
Thread-0 5
main 44
Thread-0 6
main 45
Thread-0 7
main 46
Thread-0 8
main 47
Thread-0 9
main 48
Thread-0 10
main 49
Thread-0 11
main 50
Thread-0 12
main 51
Thread-0 13
main 52
Thread-0 14
main 53
Thread-0 15
main 54
Thread-0 16
main 55
Thread-0 17
main 56
Thread-0 18
main 57
Thread-0 19
main 58
Thread-0 20
main 59
Thread-0 21
main 60
Thread-0 22
main 61
main 62
Thread-0 23
main 63
Thread-0 24
main 64
main 65
Thread-0 25
main 66
Thread-0 26
main 67
Thread-0 27
main 68
Thread-0 28
main 69
Thread-0 29
main 70
Thread-0 30
main 71
Thread-0 31
main 72
Thread-0 32
main 73
Thread-0 33
main 74
Thread-0 34
main 75
Thread-0 35
main 76
Thread-0 36
main 77
main 78
Thread-0 37
Thread-0 38
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49
Thread-0 50
Thread-0 51
Thread-0 52
Thread-0 53
Thread-0 54
Thread-0 55
Thread-0 56
Thread-0 57
main 79
main 80
main 81
main 82
main 83
Thread-0 58
main 84
Thread-0 59
main 85
Thread-0 60
main 86
Thread-0 61
main 87
Thread-0 62
main 88
Thread-0 63
main 89
Thread-0 64
main 90
Thread-0 65
main 91
Thread-0 66
main 92
Thread-0 67
main 93
Thread-0 68
main 94
Thread-0 69
main 95
Thread-0 70
main 96
Thread-0 71
main 97
Thread-0 72
main 98
Thread-0 73
main 99
Thread-0 74
主线程for循环执行完毕..
Thread-0 75
Thread-0 76
Thread-0 77
Thread-0 78
Thread-0 79
Thread-0 80
Thread-0 81
Thread-0 82
Thread-0 83
Thread-0 84
Thread-0 85
Thread-0 86
Thread-0 87
Thread-0 88
Thread-0 89
Thread-0 90
Thread-0 91
Thread-0 92
Thread-0 93
Thread-0 94
Thread-0 95
Thread-0 96
Thread-0 97
Thread-0 98
Thread-0 99
sum = 4950
执行结果
于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕..”则很可能是在子线程循环中间输出。
由CPU的线程调度机制,我们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。
上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。
2.4、synchronized的再理解
synchronized的基本规则为下面3条:
第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第一条:
package com.thread.sync; class MyRunable implements Runnable { @Override
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
} public class Demo1_1 { public static void main(String[] args) {
Runnable demo = new MyRunable(); // 新建“Runnable对象” Thread t1 = new Thread(demo, "t1"); // 新建“线程t1”, t1是基于demo这个Runnable对象
Thread t2 = new Thread(demo, "t2"); // 新建“线程t2”, t2是基于demo这个Runnable对象
t1.start(); // 启动“线程t1”
t2.start(); // 启动“线程t2”
}
}
synchronized实现互斥
run()方法中存在“synchronized(this)代码块”,而且t1和t2都是基于"demo这个Runnable对象"创建的线程。这就意味着,我们可以将synchronized(this)中的this看作是“demo这个Runnable对象”;因此,线程t1和t2共享“demo对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待“运行线程”释放“demo的同步锁”之后才能运行。
下面我们看一下线程的另一种实现方法:
package com.thread.sync;
class MyThread extends Thread { public MyThread(String name) {
super(name);
} @Override
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
} public class Demo1_2 { public static void main(String[] args) {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
Thread t2 = new MyThread("t2"); // 新建“线程t2”
t1.start(); // 启动“线程t1”
t2.start(); // 启动“线程t2”
}
}
继承Thread,同步的this是不同的对象
synchronized(this)中的this是指“当前的类对象”,即synchronized(this)所在的类对应的当前对象。它的作用是获取“当前对象的同步锁”。在Demo1_2中,synchronized(this)中的this代表的是MyThread对象,而t1和t2是两个不同的MyThread对象,因此t1和t2在执行synchronized(this)时,获取的是不同对象的同步锁。对于Demo1_1对而言,synchronized(this)中的this代表的是MyRunable对象;t1和t2共同一个MyRunable对象,因此,一个线程获取了对象的同步锁,会造成另外一个线程等待。其实对于这种情况,加不加同步锁意义并不大。
但是如果我们同步的对象换成了线程本身的类MyThread.class就不一样了,可以看到又保证了同步:
package com.thread.sync;
class MyThread extends Thread { public MyThread(String name) {
super(name);
} @Override
public void run() {
synchronized(MyThread.class) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
} public class Demo1_2 { public static void main(String[] args) {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
Thread t2 = new MyThread("t2"); // 新建“线程t2”
t1.start(); // 启动“线程t1”
t2.start(); // 启动“线程t2”
}
}
同步MyThread.class
第二条:
package com.thread.sync; class Count {
// 含有synchronized同步块的方法
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
} // 非同步的方法
public void nonSynMethod() {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
} public class Demo2_1 { public static void main(String[] args) {
final Count count = new Count();
// 新建t1, t1会调用“count对象”的synMethod()方法
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod();
}
}, "t1"); // 新建t2, t2会调用“count对象”的nonSynMethod()方法
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.nonSynMethod();
}
}, "t2"); t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
两个线程分别执行同一对象的同步和非同步代码块
第三条:
package com.thread.sync; class SynCount { // 含有synchronized同步块的方法
public void synMethod1() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod1 loop " + i);
}
} catch (InterruptedException ie) {
}
}
} // 也包含synchronized同步块的方法
public void synMethod2() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " synMethod2 loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
} public class Demo3 { public static void main(String[] args) {
final SynCount count = new SynCount();
// 新建t1, t1会调用“count对象”的synMethod1()方法
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod1();
}
}, "t1"); // 新建t2, t2会调用“count对象”的synMethod2()方法
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod2();
}
}, "t2"); t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
两个线程同时调用同一对象的不同同步代码块,只能按顺序执行
三、总结
在本文中,我们学到了太多的知识,关于线程的生命周期中的各种方法的定义和使用的案例,我们分析了很多,加深了我们对线程的理解,另外对于多线程的两种创建方法带来的执行原理等不同我们也做了分析,最后我们对synchronized关键字进行了深刻的理解和解析。在学习的道路上,我们要保持谦逊的心态,多积累,多总结,多实践才能出真知。
参考文献:https://www.cnblogs.com/skywang12345/p/3479949.html
https://www.cnblogs.com/lwbqqyumidi/p/3804883.html
http://www.cnblogs.com/skywang12345/p/3479224.html