JUC学习笔记(二)线程的基本操作

一、线程的生命周期

我们可以绘制一张简单的状态图来描述这个概念

 线程的所有状态都在Thread中的State枚举中定义,如下所示:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
  • NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法调用时,才表示线程开始执行。
  • 当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都己经准备好了。
  • 如果线程在执行过程中遇到了 synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。
  • WAITING和 TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。那么等待的线程究竟在等什么呢? 一般来说,WAITING的线程正是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。
  • 当线程执行完毕后,则进入TERMINATED状态,表示结束。

注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。 

二、线程的基本操作 

1、新建线程

使用new关键字创建一个线程对象,并且将它start()起来即可。

Thread t1 = new Thread();
t1.start();

那么线程start()后,会干什么呢?线程Thread,有一个run方法,start()方法就会新建一个线程并让这个线程执行run()方法。 

这里要注意,下面的代码通过编译,也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法只是作为一个普通的方法调用。

Thread t1 = new Thread();
t1.run();

总结:start()方法和run()方法的区别 

  • 调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();
  • 直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
  • 一个线程对线的 start() 方法只能调用一次,多次调用会抛出java.lang.IllegalThreadStateException 异常;run() 方法没有限制。

如果想让线程做些什么,就必须重写run()方法

    Thread t1 = new Thread(){
        @Override
        public void run(){
            System.out.println("hello, I am t1");
        }
    };
    t1.start();

上述代码使用匿名内部类,重写了 run()方法,并要求线程在执行时打印“Hello,I am t1” 的字样。 

考虑到Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runnable接口是一个单方法接口,它只有一个run()方法:

@FunctionalInterface
public interface Runnable {
    void run();
}

此外,Thread类有一个非常重要的构造方法:

public Thread(Runnable target)

它传入一个Runnable接口的实例,在start()方法调用时,新的线程就会执行Runnable.run()方法。实际上,默认的Thread.run()方法就是这么做的:

    public void run() {
        if (target != null) {
            target.run();
        }
    }

注意:默认的Thread.run()方法就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,更为合理。

2、终止线程

一般来说,线程执行完毕就会结束,无须手工关闭。但是,凡事都有例外。一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。比如,它们的执行体本身就是一个大大的无穷循环,用于提供某些服务。

那么如何正常地关闭一个线程呢?查阅JDK,你不难发现线程Thread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。但是stop()方法是一个被标注为废弃的方法。也就是说,在将来,JDK可能就会移除该方法。

为什么stop()方法被废弃而不推荐使用呢?

stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

先简单介绍一些有关数据不一致的概念

我们在数据库里维护着一张用户表,里面记录了用户ID和用户名。假设这里有两条记录:

记录1: ID=1, Name = 小王
记录2: ID=2, Name = 小明

如果我们用一个User对象去保存这些记录,我们总是希望这个对象要么保存记录1,要么保存记录2。如果这个User对象一半存着记录1,另外一半存着记录2,那么数据就己经不一致了,说白了就是系统有错误了。

也许有人会问,怎么可能呢?跑得好好的系统,怎么会出现这种问题呢?在单线程环境中,确实不会,但在并行程序中,如果考虑不周,就有可能出现类似的情况。不经思考地使用stop()方法就有可能导致这种问题。

Thread.stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁, 而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁己经被释放,另外一个等待该锁的读线程就顺理成章地读到了这个不一致的对象,悲剧也就此发生。

来看一个例子

对象u持有ID和NAME两个字段,假设当ID等于NAME时表示对象是一致的,否则表示对象出错。写线程总是会将ID和NAME写成相同的值,并且在这里初始值都为0。当写线程在写对象时,读线程由于无法获得锁,因此必须等待,所以读线程是看不见一个写了一半的对象的。当写线程写完ID后,很不幸地被stop(),此时对象u的ID为1 而NAME仍然为0,处于不一致状态。而被终止的写线程简单地将锁释放,读线程争夺到锁后,读取数据,于是,读到了 ID=1而NAME=0的错误值。

代码模拟

public class StopThreadUnsafe {
    private static User user = new User(0,"0");

    //写线程:总是写入两个相同的值
    public static class WriterObjectThread extends Thread{
        @Override
        public void run() {
            while (true){
                synchronized (user){
                    int v = (int) System.currentTimeMillis();
                    user.id = v;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.name = (String.valueOf(v));
                }
                Thread.yield();
            }
        }
    }

    //读线程:当读取到id和name不一样时输出
    public static class ReadObjectThread extends Thread{
        @Override
        public void run() {
            while (true){
                synchronized (user){
                    if(user.id != Integer.parseInt(user.name)){
                        System.out.println(user.toString());
                    }
                }
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReadObjectThread().start();
        while (true){
            WriterObjectThread t = new WriterObjectThread();
            t.start();
            Thread.sleep(150);//主线程睡眠150ms
            t.stop();
        }
    }
}
class User{
    public int id;
    public String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

结果:

User{id=1924626610, name='1924626499'}

User{id=1924627086, name='1924626975'}

在真实环境中,这类问题一旦出现, 就很难排查,它们甚至没有任何错误信息,也没有线程堆栈。因此,除非你很清楚自己在做什么,否则不要随便使用stop()方法来停止一个线程。

如果需要停止一个线程,那么应该怎么做呢?其实方法很简单,只需要由我们自行决定线程何时退出就可以了。仍然用本例说明,只需要将ChangeObjectThread线程增加一个stopMe()方法即可

    public static class WriterObjectThread extends Thread{
        volatile boolean stopme = false;

        public void stopMe(){
            stopme = true;
        }
        @Override
        public void run() {
            while (true){
                if(stopme){
                    System.out.println("exit by stop me");
                    break;
                }
                synchronized (user){
                    int v = (int) System.currentTimeMillis();
                    user.id = v;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.name = (String.valueOf(v));
                }
                Thread.yield();
            }
        }
    }

第2行代码定义了一个标记变量stopme,用于指示线程是否需要退出。当stopMe()方法被调用,stopme就被设置为true,此时,在第10行代码检测到这个改动时,线程就自动退出了。

使用这种方式退出线程,不会使对象user的状态出现错误。因为ChangeObjectThread己经没有机会“写坏”对象了。

3、线程中断

线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到stop()方法的老问题。

有三个方法与线程中断有关

public void interrupt() //中断线程,设置中断标志位,中断标志位表示当前线程己经被中断了。
public static boolean interrupted() //判断是否被中断
public boolean isInterrupted() //判断是否被中断,并清除当前中断状态

下面这段代码对tl线程进行了中断,那么中断后tl会停止执行吗?

    public static void main(String[] args) throws java.lang.InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

在这里,虽然对t1进行了中断,但是在t1中并没有中断处理的逻辑,因此,即使tl线程被置为中断状态,这个中断也不会发生任何作用。 

    public static void main(String[] args) throws java.lang.InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("Interruted!");
                        break;
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(200);
        t1.interrupt();
    }

这看起来与前面增加stopme标记的手法非常相似,但是中断的功能更为强劲。如果在循环体中,出现了类似于wait()方法或者sleep()方法这样的操作,则只能通过中断来识别了。 

下面,先来了解一下Thread.sleep()函数。

public static native void sleep(long millis) throws InterruptedException;

Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。

    public static void main(String[] args) throws java.lang.InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while (true){
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("Interrupted");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (java.lang.InterruptedException e) {
                        e.printStackTrace();
                        System.out.println("Interrupted when sleep!");
                        Thread.currentThread().interrupt();
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }

注意上面代码中第10〜15行,如果线程在第11行代码处被中断,则程序会抛出异常,并进入第13行代码处理。在catch子句部分,由于己经捕获了中断,我们可以立即退出线程。

但在这里,我们并没有这么做,因为也许在这段代码中,我们还必须进行后续的处理来保证数据的一致性和完整性,因此,执行了 Thread.interrupt()方法再次中断自己,置上中断标记位。只有这么做,在第6行代码的中断检查中,才能发现当前线程己经被中断了。 

Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

4、等待(wait)和通知(notify) 

为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程:等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这两个方法。

public final void wait() throws InterruptedException 
public final native void notify();

wait()方法和notify()方法究竟是如何工作的呢?

如果一个线程调用了 object.wait()方法,那么它就会进入object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。

这里还需要强调一点,object.wait()方法并不能随便调用。它必须包含在对应的synchronzied语句中,无论是wait()方法或者notify()方法都需要首先获得目标对象的一个监视器。 

代码案例

public class SimpleWN {
    final static Object o = new Object();

    public static class T1 extends Thread {
        @Override
        public void run() {
            synchronized (o) {
                System.out.println(System.currentTimeMillis() + ": T1 Start!");
                try {
                    System.out.println(System.currentTimeMillis() + ": T1 wait for Object");
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ": T1 end");
            }
        }


        public static class T2 extends Thread {
            @Override
            public void run() {
                synchronized (o) {
                    System.out.println(System.currentTimeMillis() + ": T2 Start! notify One Thread");
                    o.notify();
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(System.currentTimeMillis() + ": T2 end");
                }
            }
        }

        public static void main(String[] args) {
            T1 t1 = new T1();
            T2 t2 = new T2();
            t1.start();
            t2.start();
        }
    }
}

结果

1664086097927: T1 Start!
1664086097928: T1 wait for Object
1664086097928: T2 Start! notify One Thread
1664086099933: T2 end
1664086099933: T1 end 

注意程序打印的时间戳信息,在T2通知Tl继续执行后,T1并不能立即继续执行,而是要等待T2释放object的锁,并重新成功获得锁后,才能继续执行。因此,加粗部分时间戳的间隔为2秒 

T1在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。

wait()方法和notify()方法的工作流程细节:

注意:wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。 

5、挂起(suspend)和继续执行(resume)线程

这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续指定。乍看之下,这对操作就像Thread.stop()方法一样好用。但是它们早己被标注为废弃方法,并不推荐使用。

不推荐使用suspend()方法去挂起线程原因

  1. suspend()方法在导致线程暂停的同时,并不会释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了 resume()方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。
  2. 如果resume()方法操作意外地在suspend()方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。 
  3. 而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable,这也会严重影响我们对系统当前状态的判断。 

suspend()方法导致线程进入类似死锁的状态:

代码演示

public class BadSuspend {
    public static Object o = new Object();

    static ChangeObjectThread t1 = new ChangeObjectThread("线程1");
    static ChangeObjectThread t2 = new ChangeObjectThread("线程2");

    public static class ChangeObjectThread extends Thread{
        public ChangeObjectThread(String name){
            super(name);
        }
        @Override
        public void run(){
            synchronized (o){
                System.out.println("in " + getName());
                Thread.currentThread().suspend();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join(); //主线程等待t1 执行完毕
        t2.join();//主线程等待t2 执行完毕
    }
}

结果:得到以下输出,这表明两个线程先后进入了临界区,但是程序不会退出,而是会挂起。

in 线程1
in 线程2

使用jstack命令打印系统的线程信息可以看到: 线程t2其实是被挂起的,但是它的线程状态确实是RUNNABLE,这很有可能使我们误判当前系统的状态。同时,虽然主函数中己经调用了resume()方法,但是由于时间先后顺序的缘故,那个resume并没有生效!这就导致了线程t2被永远挂起,并且永远占用了对象u的锁。

如果需要一个比较可靠的suspend()方法,那么应该怎么办呢?

下面的代码就给出了一个利用wait()方法和notify()方法,在应用层面实现suspend()方法和resume()方法功能的例子。 

public class GoodSuspend {
    public static Object u = new Object();

    public static class ChangeObjectThread extends Thread{
        volatile boolean suspendme = false;
        //挂起线程
        public void suspendMe(){
            suspendme = true;
        }
        //唤醒线程
        public void resumeMe(){
            suspendme = false;
            synchronized (this){
                notify();
            }
        }

        @Override
        public void run() {
            while (true){
                synchronized (this) {
                    //首先检查自己是否被挂起,如果是,则执行wait()方法进行等待
                    while (suspendme) {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //正常处理
                    synchronized (u) {
                        System.out.println("in ChangeObjectThread");
                    }
                    Thread.yield();
                }
            }
        }
    }

    public static class ReadObjectThread extends Thread{
        @Override
        public void run() {
           while (true){
               synchronized (u){
                   System.out.println("in ReadObjectThread");
               }
               Thread.yield();
           }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ChangeObjectThread t1 = new ChangeObjectThread();
        ReadObjectThread t2 = new ReadObjectThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.suspendMe();
        System.out.println("suspend t1 2 sec.............................................");
        Thread.sleep(2000);
        System.out.println("resume t1.............................................");
        t1.resumeMe();
    }
}

6、等待线程结束(join)和谦让(yeild)

一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了 join()操作来实现这个功能。

public final void join() throws InterruptedException 
public final synchronized void join(long millis) throws InterruptedException 

第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。

第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。

join()方法的本质是让调用线程在当前线程对象上进行等待

while (isAlive()) {
    wait(0);
}

当线程执行完成后,被等待的线程会在退出前调用notifyAll()方法通知所有的等待线程继续执行。

Thread.yield(),它的定义如下

public static native void yield();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到就不一定了。

如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yieldO方法,给予其他重要线程更多的工作机会。

三、分门别类的管理:线程组

在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在同一个线程组里。

public class ThreadGroupName implements Runnable{
    @Override
    public void run() {
        String groupAndName = Thread.currentThread().getThreadGroup().getName()
                + "-" + Thread.currentThread().getName();
        System.out.println(groupAndName);
    }

    public static void main(String[] args) {
        ThreadGroup group = new ThreadGroup("PrintGroup");
        Thread t1 = new Thread(group, new ThreadGroupName(), "T1");
        Thread t2 = new Thread(group, new ThreadGroupName(), "T2");
        t1.start();
        t2.start();
        System.out.println(group.activeCount());
        group.list();
    }
}
  • activeCount()方法可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法精确;
  • list()方法可以打印这个线程组中所有的线程信息

线程组还有一个值得注意的方法stop(),它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()方法相同的问题,因此使用时也需要格外谨慎。

四、驻守后台:守护线程(Daemon)

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JFT线程就可以理解为守护线程。

与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象己经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机就会自然退出。

public class DaemonDemo {
    public static class DaemonT extends Thread{
        @Override
        public void run() {
            while (true){
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DaemonT daemon = new DaemonT();
        daemon.setDaemon(true);
        daemon.start();
        Thread.sleep(2000);
    }
}

由于daemon被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,那么main线程结束后,daemon线程还会不停地打印,永远不会结束。

五、先做重要的事:线程优先级

Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题。如果运气不好,那么高优先级线程可能也会抢占失败。

在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:

    public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

数字越大则优先级越高,但有效范围在1到10之间。


版权声明:本文为qq_51409098原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。