《实战高并发程序设计》读书笔记-线程基本状态和基本操作
线程的状态有哪些?
- new
- 新建状态:线程创建之后
- running
- 可运行:可能正在运行,也可能正在等待CPU时间片。
- blocked
- 阻塞:等待获取一个排它锁,如果其线程释放了锁就会结束此状态
- waiting
- 无限期等待:等待其它线程显式地唤醒,否则不会被分配CPU时间片
- time waiting
- 限期等待:无需等待其它线程显式地唤醒,在一定时间之后会被系统终止
- terminated
-
终止( /term nei tid/):可以是线程结束任务之后自己结束,或者产生了异常而结束
image.png
-
线程创建之后它将处于new(新建)状态,调用start()方法后开始运行,线程这时候处于ready(可运行)状态。可运行状态的线程获得了cpu时间片后就处于running(运行)状态(操作系统隐藏JⅥM中的ready和running状态,它只能看到 RUNNABLE状态)。
当线程执行wai()方法之后,线程进入waiting(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 time waiting(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过slep( long millis)方法或wait( long millis)方法可以将Java线程置于 timed_waiting状态。当超时时间到达后Java线程将会返回runnable状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到blocked(阻塞)状态。线程在执行 Runnable的run()方法之后将会进入到 terminated(终止)状态。
线程的基本操作
新建线程
public static void main(String[] args) {
Thread t1=new Thread(new CreateThread3());
t1.start();
}
@Override
public void run() {
System.out.println("Oh, I am Runnable");
}
}
notice:
线程调用run()和start()的区别
Thread t1=new Thread();
t1.run();
这段代码虽然也可以通过编译,但是却不是创建一个新线程,而是作为一个普通方法在当前线程中串行执行,所以不要用这种方法开启线程。
终止线程
一般情况下,线程执行完后就会结束,无需手工关闭,但是hread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。
但是stop()的使用要慎重,这过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
使用stop引起的数据不一致问题
假设为用户表赋值,他有两个属性。
记录1:ID=1,NAME=小明
记录2:ID=2,NAME=小王
如果用同一个对象去保存这个记录,使用stop会引起数据不一致问题,比如id=2,而Name=小明。
这是因为Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章的读到了这个不一致的对象。
image.png
演示代码
01 public class StopThreadUnsafe {
02 public static User u=new User();
03 public static class User{
04 private int id;
05 private String name;
06 public User(){
07 id=0;
08 name="0";
09 }
10 //省略setter和getter方法
11 @Override
12 public String toString() {
13 return "User [id=" + id + ", name=" + name + "]";
14 }
15 }
16 public static class ChangeObjectThread extends Thread{
17 @Override
18 public void run(){
19 while(true){
20 synchronized(u){
21 int v=(int)(System.currentTimeMillis()/1000);
22 u.setId(v);
23 //Oh, do sth. else
24 try {
25 Thread.sleep(100);
26 } catch (InterruptedException e) {
27 e.printStackTrace();
28 }
29 u.setName(String.valueOf(v));
30 }
31 Thread.yield();
32 }
33 }
34 }
35
36 public static class ReadObjectThread extends Thread{
37 @Override
38 public void run(){
39 while(true){
40 synchronized(u){
41 if(u.getId() != Integer.parseInt(u.getName())){
42 System.out.println(u.toString());
43 }
44 }
45 Thread.yield();
46 }
47 }
48 }
49
50 public static void main(String[] args) throws InterruptedException {
51 new ReadObjectThread().start();
52 while(true){
53 Thread t=new ChangeObjectThread();
54 t.start();
55 Thread.sleep(150);
55 Thread.sleep(150);
56 t.stop();
57 }
58 }
59 }
执行以上代码,可以很容易得到类似如下输出,ID和NAME产生了不一致。
User [id=1425135593, name=1425135592]
User [id=1425135594, name=1425135593]
如果在线上环境跑出以上结果,那么加班加点估计是免不了了,因为这类问题一旦出现,就很难排查,因为它们甚至没有任何错误信息,也没有线程堆栈。这种情况一旦混杂在动则十几万行的程序代码中时,发现它们就全凭经验、时间还有一点点运气了。因此,除非你很清楚你在做什么,否则不要随便使用stop()方法来停止一个线程。
这种问题如何规避?
方案一
可以看到上文中的User是一个全局变量,存储在堆中,线程共用,而且这个也是线程的共享变量,如果做不到原子操作,必然会会有线程安全问题,最好的方法,线程之间不用这种存储在heap中的变量,将这个对象放到方法中创建,即对象存储到stack中。
方案二
在线程中设置一个标记变量,作为while循环的条件,就算标记变量改为终止线程,也不会立刻结束,会等待当前循环的逻辑处理完后,再终止线程。
01 public static class ChangeObjectThread extends Thread {
02 volatile boolean stopme = false;
03
04 public void stopMe(){
05 stopme = true;
06 }
07 @Override
08 public void run() {
09 while (true) {
10 if (stopme){
11 System.out.println("exit by stop me");
12 break;
13 }
14 synchronized (u) {
15 int v = (int) (System.currentTimeMillis() / 1000);
16 u.setId(v);
17 //Oh, do sth. else
18 try {
19 Thread.sleep(100);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 u.setName(String.valueOf(v));
24 }
25 Thread.yield();
26 }
27 }
28 }
线程中断
这个线程中断和上文提到的线程终止不是同一个意思,线程中断强调的是给线程发送一个通知,而不是让线程立刻终止,至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
jdk提供的线程中断的三个方法
public void Thread.interrupt() // 中断线程
public boolean Thread.isInterrupted() // 判断是否被中断
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态
Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。(可以看出这和上面提到的解决数据不一致的方案二的思路如出一辙)中断标志位表示当前线程已经被中断了。
Thread.isInterrupted()方法也是实例方法,它判断当前线程是否有被中断(通过检查中断标志位)。
静态方法Thread.interrupted()也是用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。
调用中断会终止线程么?
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
在这里,虽然对t1进行了中断,但是在t1中并没有中断处理的逻辑,因此,即使t1线程被置上了中断状态,但是这个中断不会发生任何作用。
如果希望t1在中断后退出,就必须为它增加相应的中断处理代码:
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//这才是通过标记位判断是否终止线程的正确方式
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
break;
}
Thread.yield();
}
}
只有在线程的业务逻辑中添加
if(Thread.currentThread().isInterrupted())
才是正确中断线程的方式,否则单独使用interrupt()起不到终止线程的目的。
isInterrupted()和Thread.sleep()组合使用的正确方式
如果在循环体中,出现了类似于wait()或者sleep()这样的操作,则只能通过中断来识别了。
Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
01 public static void main(String[] args) throws InterruptedException {
02 Thread t1=new Thread(){
03 @Override
04 public void run(){
05 while(true){
06 if(Thread.currentThread().isInterrupted()){
07 System.out.println("Interruted!");
08 break;
09 }
10 try {
11 Thread.sleep(2000);
12 } catch (InterruptedException e) {
13 System.out.println("Interruted When Sleep");
14 //设置中断状态
15 Thread.currentThread().interrupt();
16 }
17 Thread.yield();
18 }
19 }
20 };
21 t1.start();
22 Thread.sleep(2000);
23 t1.interrupt();
24 }
注意上述代码中第10~15行加粗部分,如果在第11行代码处,线程被中断,则程序会抛出异常,并进入第13行处理。在catch子句部分,由于已经捕获了中断,我们可以立即退出线程。但在这里,我们并没有这么做,因为也许在这段代码中,我们还必须进行后续的处理,保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法再次中断自己,置上中断标记位。只有这么做,在第6行的中断检查中,才能发现当前线程已经被中断了。
注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。
等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这两个方法。
public final void wait() throws InterruptedException
public final native void notify()
当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待,转为等待状态,一直等到其他线程调用了obj.notify()方法为止。这时,obj对象就俨然成为多个线程之间的有效通信手段。Object.wait()方法并不是可以随便调用的,它必须包含在对应的synchronzied语句中,无论是wait()或者notify()都需要首先获得目标对象的一个监视器,即要是想使用waitI()必须先获取锁。而wait()方法在执行后,会释放这个监视器。这样做的目的是使得其他等待在object对象上的线程不至于因为该线程的休眠而全部无法正常执行。
需要注意的是,object.notify()唤醒线程,它就会从这个等待队列中,随机选择一个线程,并将其唤醒,这个选择是不公平的,并不是先等待的线程会优先被选择,这个选择完全是随机的。所以有些时候为了避免死循环,直接使用notifyAll(),然后先获取锁的线程得以执行业务逻辑。
等待和通知的例子
01 public class SimpleWN {
02 final static Object object = new Object();
03 public static class T1 extends Thread{
04 public void run()
05 {
06 synchronized (object) {
07 System.out.println(System.currentTimeMillis()+":T1 start! ");
08 try {
09 System.out.println(System.currentTimeMillis()+":T1 wait for object ");
10 object.wait();
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 System.out.println(System.currentTimeMillis()+":T1 end!");
15 }
16 }
17 }
18 public static class T2 extends Thread{
19 public void run()
20 {
21 synchronized (object) {
22 System.out.println(System.currentTimeMillis()+":T2 thread");
23 object.notify();
24 System.out.println(System.currentTimeMillis()+":T2 end!");
25 try {
26 Thread.sleep(2000);
27 } catch (InterruptedException e) {
28 }
29 }
30 }
31 }
32 public static void main(String[] args) {
33 Thread t1 = new T1() ;
34 Thread t2 = new T2() ;
35 t1.start();
36 t2.start();
37 }
38 }
挂起(suspend)和继续执行(resume)线程
线程挂起(suspend)和继续执行(resume)。这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()操作后,才能继续指定,但是jdk早已将他们标注为废弃方法,并不推荐使用,suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连。
等待线程结束(join)和谦让(yield)
join
很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
这里提供一个简单点的join()实例,供大家参考:
public class JoinMain {
public volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at=new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
主函数中,如果不使用join()等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,故在join()返回时,AddThread已经执行完成,故i总是10000000。
有关join(),我还想再补充一点,join()的本质是让调用线程wait()在当前线程对象实例上。下面是JDK中join()实现的核心代码片段:
while (isAlive()) {
wait(0);
}
可以看到,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行。因此,值得注意的一点是:不要在应用程序中,在Thread对象实例上使用类似wait()或者notify()等方法,因为这很有可能会影响系统API的工作,或者被系统API所影响。
yield
public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。因此,对Thread.yield()的调用就好像是在说:我已经完成一些最重要的工作了,我应该是可以休息一下了,可以给其他线程一些工作机会啦!
如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。