乐趣区

java高并发系列-第6天线程的基本操作

新建线程

新建线程很简单。只需要使用 new 关键字创建一个线程对象,然后调用它的 start()启动线程即可。

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

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

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

Thread thread1 = new Thread1();
thread1.run();

所以,希望大家注意,调用 start 方法和直接调用 run 方法的区别。

start 方法是启动一个线程,run 方法只会在当前线程中串行的执行 run 方法中的代码。

默认情况下,线程的 run 方法什么都没有,启动一个线程之后马上就结束了,所以如果你需要线程做点什么,需要把您的代码写到 run 方法中,所以必须重写 run 方法。

Thread thread1 = new Thread() {
            @Override
            public void run() {System.out.println("hello, 我是一个线程!");
            }
        };
thread1.start();

上面是使用匿名内部类实现的,重写了 Thread 的 run 方法,并且打印了一条信息。我们可以通过继承 Thread 类,然后重写 run 方法,来自定义一个线程。但考虑 java 是单继承的,从扩展性上来说,我们实现一个接口来自定义一个线程更好一些,java 中刚好提供了 Runnable 接口来自定义一个线程。

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

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

public Thread(Runnable target)
我们在看一下 Thread 的 run 方法:

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

当我们启动线程的 start 方法之后,线程会执行 run 方法,run 方法中会调用 Thread 构造方法传入的 target 的 run 方法。

实现 Runnable 接口是比较常见的做法,也是推荐的做法。

终止线程

一般来说线程执行完毕就会结束,无需手动关闭。但是如果我们想关闭一个正在运行的线程,有什么方法呢?可以看一下 Thread 类中提供了一个 stop()方法,调用这个方法,就可以立即将一个线程终止,非常方便。

package com.itsoku.chat01;

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;

/**
 * <b>description</b>:<br>
 * <b>time</b>:2019/7/12 17:18 <br>
 * <b>author</b>:专注于 java 技术分享(带你玩转 爬虫、分布式事务、异步消息服务、任务调度、分库分表、大数据等),喜欢请关注!*/
@Slf4j
public class Demo01 {public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread() {
            @Override
            public void run() {log.info("start");
                boolean flag = true;
                while (flag) {;}
                log.info("end");
            }
        };
        thread1.setName("thread1");
        thread1.start();
        // 当前线程休眠 1 秒
        TimeUnit.SECONDS.sleep(1);
        // 关闭线程 thread1
        thread1.stop();
        // 输出线程 thread1 的状态
        log.info("{}", thread1.getState());
        // 当前线程休眠 1 秒
        TimeUnit.SECONDS.sleep(1);
        // 输出线程 thread1 的状态
        log.info("{}", thread1.getState());
    }
}

运行代码,输出:

18:02:15.312 [thread1] INFO com.itsoku.chat01.Demo01 – start
18:02:16.311 [main] INFO com.itsoku.chat01.Demo01 – RUNNABLE
18:02:17.313 [main] INFO com.itsoku.chat01.Demo01 – TERMINATED
代码中有个死循环,调用 stop 方法之后,线程 thread1 的状态变为 TERMINATED(结束状态),线程停止了。

我们使用 idea 或者 eclipse 的时候,会发现这个方法是一个废弃的方法,也就是说,在将来,jdk 可能就会移除该方法。

stop 方法为何会被废弃而不推荐使用?stop 方法过于暴力,强制把正在执行的方法停止了。

大家是否遇到过这样的场景:电力系统需要维修,此时咱们正在写代码,维修人员直接将电源关闭了,代码还没保存的,是不是很崩溃,这种方式就像直接调用线程的 stop 方法类似。线程正在运行过程中,被强制结束了,可能会导致一些意想不到的后果。可以给大家发送一个通知,告诉大家保存一下手头的工作,将电脑关闭。

线程中断

在 java 中,线程中断是一种重要的线程写作机制,从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。在上面中,我们已经详细讨论了 stop 方法停止线程的坏处,jdk 中提供了更好的中断线程的方法。严格的说,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出了!至于目标线程接收到通知之后如何处理,则完全由目标线程自己决定,这点很重要,如果中断后,线程立即无条件退出,我们又会到 stop 方法的老问题。

Thread 提供了 3 个与线程中断有关的方法,这 3 个方法容易混淆,大家注意下:

public void interrupt() // 中断线程
public boolean isInterrupted() // 判断线程是否被中断
public static boolean interrupted() // 判断线程是否被中断,并清除当前中断状态
interrupt() 方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位为 true,中断标志位表示当前线程已经被中断了。isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)。最后一个方法 interrupted()是一个静态方法,返回 boolean 类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。

while (true) {if (this.isInterrupted()) {System.out.println("我要退出了!");
                break;
            }
        }
    }
};
thread1.setName("thread1");
thread1.start();
TimeUnit.SECONDS.sleep(1);
thread1.interrupt();

上面代码中有个死循环,interrupt()方法被调用之后,线程的中断标志将被置为 true,循环体中通过检查线程的中断标志是否为 ture(this.isInterrupted())来判断线程是否需要退出了。

再看一种中断的方法:

static volatile boolean isStop = false;

public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread() {
        @Override
        public void run() {while (true) {if (isStop) {System.out.println("我要退出了!");
                    break;
                }
            }
        }
    };
    thread1.setName("thread1");
    thread1.start();
    TimeUnit.SECONDS.sleep(1);
    isStop = true;
}

代码中通过一个变量 isStop 来控制线程是否停止。

通过变量控制和线程自带的 interrupt 方法来中断线程有什么区别呢?

如果一个线程调用了 sleep 方法,一直处于休眠状态,通过变量控制,还可以中断线程么?大家可以思考一下。

此时只能使用线程提供的 interrupt 方法来中断线程了。

public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread() {
        @Override
        public void run() {while (true) {
                // 休眠 100 秒
                try {TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println("我要退出了!");
                break;
            }
        }
    };
    thread1.setName("thread1");
    thread1.start();
    TimeUnit.SECONDS.sleep(1);
    thread1.interrupt();}

调用 interrupt()方法之后,线程的 sleep 方法将会抛出 InterruptedException 异常。

Thread thread1 = new Thread() {
    @Override
    public void run() {while (true) {
            // 休眠 100 秒
            try {TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            if (this.isInterrupted()) {System.out.println("我要退出了!");
                break;
            }
        }
    }
};

运行上面的代码,发现程序无法终止。为什么?

代码需要改为:

Thread thread1 = new Thread() {
    @Override
    public void run() {while (true) {
            // 休眠 100 秒
            try {TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {this.interrupt();
                e.printStackTrace();}
            if (this.isInterrupted()) {System.out.println("我要退出了!");
                break;
            }
        }
    }
};

上面代码可以终止。

注意:sleep 方法由于中断而抛出异常之后,线程的中断标志会被清除(置为 false),所以在异常中需要执行 this.interrupt()方法,将中断标志位置为 true

等待(wait)和通知(notify)

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

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

当在一个对象实例上调用 wait()方法后,当前线程就会在这个对象上等待。这是什么意思?比如在线程 A 中,调用了 obj.wait()方法,那么线程 A 就会停止继续执行,转为等待状态。等待到什么时候结束呢?线程 A 会一直等到其他线程调用 obj.notify()方法为止,这时,obj 对象成为了多个线程之间的有效通信手段。

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

除 notify()方法外,Object 独享还有一个 nofiyAll()方法,它和 notify()方法的功能类似,不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。

这里强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的 synchronize 语句汇总,无论是 wait()方法或者 notify()方法都需要首先获取目标独享的一个监视器。图 2.6 显示了 wait()方法和 nofiy()方法的工作流程细节。其中 T1 和 T2 表示两个线程。T1 在正确执行 wait()方法钱,必须获得 object 对象的监视器。而 wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在 object 对象上的线程不至于因为 T1 的休眠而全部无法正常执行。

线程 T2 在 notify()方法调用前,也必须获得 object 对象的监视器。所幸,此时 T1 已经释放了这个监视器,因此,T2 可以顺利获得 object 对象的监视器。接着,T2 执行了 notify()方法尝试唤醒一个等待线程,这里假设唤醒了 T1。T1 在被唤醒后,要做的第一件事并不是执行后续代码,而是要尝试重新获得 object 对象的监视器,而这个监视器也正是 T1 在 wait()方法执行前所持有的那个。如果暂时无法获得,则 T1 还必须等待这个监视器。当监视器顺利获得后,T1 才可以在真正意义上继续执行。

给大家上个例子:

package com.itsoku.chat01;

/**
 * <b>description</b>:<br>
 * <b>time</b>:2019/7/12 17:18 <br>
 * <b>author</b> 专注于 java 技术分享(带你玩转 爬虫、分布式事务、异步消息服务、任务调度、分库分表、大数据等),喜欢请关注!*/
public class Demo06 {static Object object = new Object();

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

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

    public static void main(String[] args) throws InterruptedException {new T1().start();
        new T2().start();
    }
}

运行结果:

1562934497212:T1 start!
1562934497212:T1 wait for object
1562934497212:T2 start,notify one thread!
1562934497212:T2 end!
1562934499213:T1 end!
注意下打印结果,T2 调用 notify 方法之后,T1 并不能立即继续执行,而是要等待 T2 释放 objec 投递锁之后,T1 重新成功获取锁后,才能继续执行。因此最后 2 行日志相差了 2 秒(因为 T2 调用 notify 方法后休眠了 2 秒)。

注意:Object.wait()方法和 Thread.sleeep()方法都可以让现场等待若干时间。除 wait()方法可以被唤醒外,另外一个主要的区别就是 wait()方法会释放目标对象的锁,而 Thread.sleep()方法不会释放锁。

再给大家讲解一下 wait(),notify(),notifyAll(),加深一下理解:

可以这么理解,obj 对象上有 2 个队列,如图 1,q1:等待队列,q2:准备获取锁的队列;两个队列都为空。

obj.wait()过程:

synchronize(obj){obj.wait();
}

假如有 3 个线程,t1、t2、t3 同时执行上面代码,t1、t2、t3 会进入 q2 队列,如图 2,进入 q2 的队列的这些线程才有资格去争抢 obj 的锁,假设 t1 争抢到了,那么 t2、t3 机型在 q2 中等待着获取锁,t1 进入代码块执行 wait()方法,此时 t1 会进入 q1 队列,然后系统会通知 q2 队列中的 t2、t3 去争抢 obj 的锁,抢到之后过程如 t1 的过程。最后 t1、t2、t3 都进入了 q1 队列,如图 3。

上面过程之后,又来了线程 t4 执行了 notify()方法,如下:**

synchronize(obj){obj.notify();
}

t4 会获取到 obj 的锁,然后执行 notify()方法,系统会从 q1 队列中随机取一个线程,将其加入到 q2 队列,假如 t2 运气比较好,被随机到了,然后 t2 进入了 q2 队列,如图 4,进入 q2 的队列的锁才有资格争抢 obj 的锁,t4 线程执行完毕之后,会释放 obj 的锁,此时队列 q2 中的 t2 会获取到 obj 的锁,然后继续执行,执行完毕之后,q1 中包含 t1、t3,q2 队列为空,如图 5

接着又来了个 t5 队列,执行了 notifyAll()方法,如下:

synchronize(obj){obj.notifyAll();
}

2. 调用 obj.wait()方法,当前线程会加入队列 queue1,然后会释放 obj 对象的锁

t5 会获取到 obj 的锁,然后执行 notifyAll()方法,系统会将队列 q1 中的线程都移到 q2 中,如图 6,t5 线程执行完毕之后,会释放 obj 的锁,此时队列 q2 中的 t1、t3 会争抢 obj 的锁,争抢到的继续执行,未增强到的带锁释放之后,系统会通知 q2 中的线程继续争抢索,然后继续执行,最后两个队列中都为空了。

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

Thread 类中还有 2 个方法,即线程挂起 (suspend) 和继续执行 (resume),这 2 个操作是一对相反的操作,被挂起的线程,必须要等到 resume() 方法操作后,才能继续执行。系统中已经标注着 2 个方法过时了,不推荐使用。

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

上个例子:

/**
 * <b>description</b>:<br>
 * <b>time</b>:2019/7/12 17:18 <br>
 * <b>author</b>:专注于 java 技术分享(带你玩转 爬虫、分布式事务、异步消息服务、任务调度、分库分表、大数据等),喜欢请关注!*/
public class Demo07 {static Object object = new Object();

    public static class T1 extends Thread {public T1(String name) {super(name);
        }

        @Override
        public void run() {synchronized (object) {System.out.println("in" + this.getName());
                Thread.currentThread().suspend();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {T1 t1 = new T1("t1");
        t1.start();
        Thread.sleep(100);
        T1 t2 = new T1("t2");
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();}
}

运行代码输出:

in t1
in t2
我们会发现程序不会结束,线程 t2 被挂起了,导致程序无法结束,使用 jstack 命令查看线程堆栈信息可以看到:

“t2” #13 prio=5 os_prio=0 tid=0x000000002796c000 nid=0xa3c runnable [0x000000002867f000]
java.lang.Thread.State: RUNNABLE

    at java.lang.Thread.suspend0(Native Method)
    at java.lang.Thread.suspend(Thread.java:1029)
    at com.itsoku.chat01.Demo07$T1.run(Demo07.java:20)
    - locked <0x0000000717372fc0> (a java.lang.Object)

发现 t2 线程在 suspend0 处被挂起了,t2 的状态竟然还是 RUNNABLE 状态,线程明明被挂起了,状态还是运行中容易导致我们队当前系统进行误判,代码中已经调用 resume()方法了,但是由于时间先后顺序的缘故,resume 并没有生效,这导致了 t2 永远滴被挂起了,并且永远占用了 object 的锁,这对于系统来说可能是致命的。

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

很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖的线程执行完毕,才能继续执行。jdk 提供了 join()操作来实现这个功能。如下所示,显示了 2 个 join()方法:

public final void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
第 1 个方法表示无限等待,它会一直只是当前线程。知道目标线程执行完毕。

第 2 个方法有个参数,用于指定等待时间,如果超过了给定的时间目标线程还在执行,当前线程也会停止等待,而继续往下执行。

比如:线程 T1 需要等待 T2、T3 完成之后才能继续执行,那么在 T1 线程中需要分别调用 T2 和 T3 的 join()方法。

上个示例:

/**
 * <b>description</b>:<br>
 * <b>time</b>:2019/7/12 17:18 <br>
 * <b>author</b>:专注于 java 技术分享(带你玩转 爬虫、分布式事务、异步消息服务、任务调度、分库分表、大数据等),喜欢请关注!*/
public class Demo08 {
    static int num = 0;

    public static class T1 extends Thread {public T1(String name) {super(name);
        }

        @Override
        public void run() {System.out.println(System.currentTimeMillis() + ",start" + this.getName());
            for (int i = 0; i < 10; i++) {
                num++;
                try {Thread.sleep(200);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
            System.out.println(System.currentTimeMillis() + ",end" + this.getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {T1 t1 = new T1("t1");
        t1.start();
        t1.join();
        System.out.println(System.currentTimeMillis() + ",num =" + num);
    }
}

执行结果:

1562939889129,start t1
1562939891134,end t1
1562939891134,num = 10
num 的结果为 10,1、3 行的时间戳相差 2 秒左右,说明主线程等待 t1 完成之后才继续执行的。

看一下 jdk1.8 中 Thread.join()方法的实现:

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

long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {while (isAlive()) {wait(0);
    }
} else {while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {break;}
        wait(delay);
        now = System.currentTimeMillis() - base;}
}

}
从 join 的代码中可以看出,在被等待的线程上使用了 synchronize,调用了它的 wait()方法,线程最后执行完毕之后,系统会自动调用它的 notifyAll()方法,唤醒所有在此线程上等待的其他线程。

注意:被等待的线程执行完毕之后,系统自动会调用该线程的 notifyAll()方法。所以一般情况下,我们不要去在线程对象上使用 wait()、notify()、notifyAll()方法。

另外一个方法是 Thread.yield(),他的定义如下:

public static native void yield();
yield 是谦让的意思,这是一个静态方法,一旦执行,它会让当前线程出让 CPU,但需要注意的是,出让 CPU 并不是说不让当前线程执行了,当前线程在出让 CPU 后,还会进行 CPU 资源的争夺,但是能否再抢到 CPU 的执行权就不一定了。因此,对 Thread.yield()方法的调用好像就是在说:我已经完成了一些主要的工作,我可以休息一下了,可以让 CPU 给其他线程一些工作机会了。

如果觉得一个线程不太重要,或者优先级比较低,而又担心此线程会过多的占用 CPU 资源,那么可以在适当的时候调用一下 Thread.yield()方法,给与其他线程更多的机会。

总结

创建线程的 2 中方式:继承 Thread 类;实现 Runnable 接口
启动线程:调用线程的 start()方法
终止线程:调用线程的 stop()方法,方法已过时,建议不要使用
线程中断相关的方法:调用线程实例 interrupt()方法将中断标志置为 true;使用线程实例方法 isInterrupted()获取中断标志;调用 Thread 的静态方法 interrupted()获取线程是否被中断,此方法调用之后会清除中断标志(将中断标志置为 false 了)
wait、notify、notifyAll 方法,这块比较难理解,可以回过头去再理理
线程挂起使用线程实例方法 suspend(),恢复线程使用线程实例方法 resume(),这 2 个方法都过时了,不建议使用
等待线程结束:调用线程实例方法 join()
出让 cpu 资源:调用线程静态方法 yeild()

喜欢就关注我吧

退出移动版