Java线程-基础类Thread学习(二)

2018-07-12  本文已影响22人  骑着乌龟去看海

一. 前言

  Thread类作为线程中最基础的类,本篇文章我们就来了解下该类的使用。

二、. Thread类

1. 继承结构及属性

首先,来看一下Thread类的继承结构及基础属性:

public
class Thread implements Runnable {
    //...
    private volatile char  name[];
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;

    /* Whether or not the thread is a daemon thread. */
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;
}

从上面可以看出,Thread类是实现了Runnable接口,其中的一些属性:name表示的是Thread的名称,priority表示线程的优先级,daemon表示是否是守护线程,stillborn表示虚拟机状态,target表示实际要执行的任务,其中线程优先级最大是10,最小是1,默认是5。

// 线程优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

接下来我们主要来看一下Thread类的常用的方法。

2. 主要方法
2.1 start方法

   start方法,用来启动一个线程,当执行了start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源;注意不能重复调用start方法,调用start方法进行操作之后,Thread内部用于维护线程状态的变量threadStatus会相应的发生变化。

// 私有变量,线程状态
private volatile int threadStatus = 0;
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // ...
}
2.2 run方法

   run 方法,首先run方法是不需要用户手动调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便会进入run方法体去执行具体的任务。所以,继承Thread类之后一定要重写run方法。不过该run方法实际上不是Thread类本身的,而是继承自Runnable接口中的run方法。

2.3 sleep方法

  sleep 方法,让线程进入休眠状态,让出所占用的CPU资源,从上文我们可以知道,调用sleep方法之后,将会进入线程的TIMED_WAITING状态,也就是休眠状态。

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

  sleep方法需要注意的一点是该方法不会释放锁,也就是说如果当前线程持有某个对象的锁(比如说添加了Synchronized关键字),然后调用了sleep方法,那其他线程将无法访问这个对象,因为虽然该线程休眠了,但该对象的锁没有释放。只有当该线程执行完成释放锁之后,其他线程才可以继续访问。

2.4 yield方法

   yield 方法,调用该方法会让当前线程让出CPU资源,让CPU去执行与该线程具有相同优先级或更高优先级的线程。让出CPU资源后,线程由RUNNING状态重回RUNNABLE就绪状态,等待CPU的下此调用。yield方法和sleep方法有一点是相似的,就是都不会释放锁:

public static native void yield();

  不过需要注意的是,yield方法的目的是让同等或更高优先级的线程能轮换执行,但这并不是绝对的,只能表示调用该方法之后,同等或更高优先级的线程有更高的机率来去执行,就和线程的优先级不是绝对的是一个道理。这里参考知乎:https://www.zhihu.com/question/35926652

2.5 join方法

  join方法,比如两个线程,当前主线程main和主线程中创建的线程thread,调用thread线程的join方法,这时候主线程会获得thread对象的锁,然后持有thread对象锁的线程会被挂起,也就是会阻塞当前主线程。接着去执行thread线程,直至thread线程中的代码执行完成或者执行一段时间之后,才会接着执行主线程。来看一个例子:

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("进入线程:" + Thread.currentThread().getName());
        MyThread thread1 = new MyThread();
        thread1.start();

        System.out.println("线程" + Thread.currentThread().getName() + "等待");
        thread1.join();
        System.out.println("线程" + Thread.currentThread().getName() + "继续执行");

    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("----------------------------");
        System.out.println("进入线程: " + Thread.currentThread().getName());
        try {
            System.out.println("线程" + Thread.currentThread().getName() + "休眠5秒");
            sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "执行完毕");
        System.out.println("----------------------------");
    }
}

打印:

进入线程:main
线程main等待
----------------------------
进入线程: Thread-0
线程Thread-0休眠5秒
线程Thread-0执行完毕
----------------------------
线程main继续执行

例子引自:海子-Thread类的使用 ,可以看出,调用了thread1的join方法之后,main线程获取thread1对象的锁并进入阻塞状态,然后等待thread1线程执行完成之后再继续执行。join方法有三个重载方法,可以执行当前线程挂起的时间:

public final void join() throws InterruptedException {

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

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

并且,join底层是通过Object的wait方法来实现,wait会让线程进入阻塞状态, 并且释放线程占有的锁,并交出CPU的占有资源。

不过需要注意的是,我们在main()方法里 调用了thread的join方法只会阻塞main方法所在的线程,因为join方法上的synchronized关键字的特性,哪个线程正在调用这个方法,哪个线程就会获取这个锁,然后该线程就会被挂起。所以说并不会阻塞其他相关的线程,比如:

// 添加线程测试类
class MyThread2 extends Thread {
    @Override
    public void run() {
        System.out.println("进入线程: " + Thread.currentThread().getName());
        System.out.println("线程" + Thread.currentThread().getName() + "执行完毕");
    }
}

// main方法省略,其他不变
MyThread2 thread2 = new MyThread2();
MyThread thread1 = new MyThread();
thread2.start();
thread1.start();

// ...
thread1.join();
// ...

打印结果:

进入线程:main
线程main等待
进入线程: Thread-0
----------------------------
进入线程: Thread-1
线程Thread-0执行完毕
线程Thread-1休眠5秒
线程Thread-1执行完毕
----------------------------
线程main继续执行

由于线程的不确定性,打印结果会稍有不同,但MyThread2的线程并没有阻塞。另外线程挂起之后的唤醒操作,在Java源码中并没有体现,这块的实现是在JVM的源码中:

//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;

//这家伙是啥,就是一个线程执行完毕之后,jvm会做的事,做清理啊收尾工作,
//里面有一个贼不起眼的一行代码,眼神不好还看不到的呢,就是这个:

ensure_join(this);

//翻译成中文叫 确保_join(这个);代码如下:

static void ensure_join(JavaThread* thread) {
  Handle threadObj(thread, thread->threadObj());

  ObjectLocker lock(threadObj, thread);

  thread->clear_pending_exception();

  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);

  java_lang_Thread::set_thread(threadObj(), NULL);

  //thread就是当前线程,是啥是啥?就是刚才说的b线程啊。
  lock.notify_all(thread);

  thread->clear_pending_exception();
}

这里JVM的源码转载自:https://www.zhihu.com/question/44621343/answer/97640972

2.6 interrupt方法

  interrupt方法,翻译为中断的意思,但interrupt方法的实际作用并不是中断线程,而是 "通知线程应该中断了" ,然后修改线程的中断状态,具体到底中断还是继续运行,应该由被通知的线程自己处理。

具体来说,当对一个线程,调用 interrupt() 时:

  1. 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,仅此而已;
  2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响;

也就是说,interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行,也就是说,一个线程如果有被中断的需求,那么就可以这样做:

  1. 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  2. 在调用阻塞方法时正确处理InterruptedException异常;(例如,catch异常后就结束线程)

我们有两个方法来判断线程中断标志位的状态:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}
  1. Thread.interrupted(),静态方法,通过源码我们可以知道,这个方法是用于判断当前线程是否是中断状态,并且执行后会清除中断状态的标识。
  2. isInterrupted,实例方法,同样是判断线程是否是中断状态,但判断的线程是执行该方法的线程,并且执行后并不会清除中断状态的标识。这一点,它们的源码很清晰的就说明了。

来看一个简单的例子:

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread();
        thread1.start();
        thread1.interrupt();
        System.out.println("线程 " + thread1.getName() + "是否终止状态:" + thread1.isInterrupted());
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i< 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print(i + " ");
        }
        System.out.println("线程执行完成: " + Thread.currentThread().getName());
    }
}

打印结果:

java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at jdk8.stream.MyThread.run(ThreadTest.java:17)
线程 Thread-0是否终止状态:true
0 1 2 3 4 线程执行完成: Thread-0

  从上面我们可以看出,虽然调用了interrupt方法,但线程并没有结束,只是线程的中断状态修改为了true,并且对于sleep中的线程,也仅仅是抛出了一个异常而已。

这里参考自:https://www.zhihu.com/question/41048032/answer/89431513

2.7 一些get、set或者is方法
public boolean isInterrupted()
public final native boolean isAlive();
public final void setPriority(int newPriority)
public final int getPriority() 
public final synchronized void setName(String name)
public final String getName()
public final ThreadGroup getThreadGroup()
public final void setDaemon(boolean on)
public final boolean isDaemon()
public StackTraceElement[] getStackTrace()
public static Map<Thread, StackTraceElement[]> getAllStackTraces()

比如,我们想获取当前JVM中所有正在运行的线程:

Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread thread : threadSet) {
    System.out.println(thread.getName());
}
public long getId() {
    return tid;
}
public enum State {
    NEW,

    RUNNABLE,

    BLOCKED,
  
    WAITING,
   
    TIMED_WAITING,

    TERMINATED;
}
2.8 其他方法
  1. currentThread 方法,静态方法,返回当前线程对象;
  1. activeCount,静态方法,返回当前线程组及其子组中活动线程的一个估计值,该方法会递归的遍历当前线程的线程组中的所有子组,返回的值只是一个估计值,因为当此方法遍历内部数据结构时,线程的数量可能会动态变化,并且可能会受到某些系统线程的存在的影响。此方法主要用于调试和监视目的。
  1. enumerate,静态方法,将当前线程组及其子组中的每个活动线程复制到指定的数组中。和 activeCount 方法是对应的,该方法通过调用ThreadGroup.enumerate()方法来实现。同样,该方法主要用于调试和监视目的。

可以简单的看个例子:
(代码来自:http://www.tutorialspoint.com/java/lang/thread_enumerate.htm)

public static void main(String[] args) {
    Thread t = Thread.currentThread();
    t.setName("Admin Thread");
    System.out.println("current thread = " + t);

    int count = Thread.activeCount();
    System.out.println("currently active threads = " + count);

    Thread th[] = new Thread[count];
    // returns the number of threads put into the array
    Thread.enumerate(th);

    // output
    for (int i = 0; i < count; i++) {
        System.out.println(i + ": " + th[i]);
    }
}

output:

current thread = Thread[Admin Thread,5,main]
currently active threads = 2
0: Thread[Admin Thread,5,main]
1: Thread[Monitor Ctrl-Break,5,main]
  1. dumpStack,静态方法,打印当前线程的堆栈跟踪信息,主要用于调试;
  1. checkAccess,final类型的实例方法,用于确认当前运行的线程是否具有修改该线程的权限;
  1. holdsLock,静态方法,当且仅当 当前线程持有指定对象上的监视锁时,返回true;

3. 已废弃方法

  前面列举的都是目前版本(JDK 8)中正在使用的方法,除了这些方法之外,Thread还有一些已经废弃不推荐使用的方法。被废弃的方法大致有如下几个:stopdestroysuspendresumecountStackFrames这几个。
  被废弃的原因是因为这些方法基本上都是不安全的,使用的时候或多或少会出现一些额外的问题,比如stop方法,会停止run方法的运行,然后释放掉对应的锁,这时候锁所保护的临界区就有可能出现状态不一致的情况,而这一状态有可能会暴露给其他线程,造成最终对象不一致的情况;而suspend 方法则自带出现死锁的可能性,更详细的原因官网有详细的解释:
Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?

三、Object类的几个方法

  最后了,我们再来看一下Object的两个方法:notifywait方法,这两个方法虽然是Object类的方法,但与Thread类是有很大的关系的,我们来了解下:

  1. notify,用于唤醒处于WAITING状态的线程,如果该对象上有多个等待的线程,则任意选择其中的一个线程进行唤醒;
  2. notifyAll,唤醒该对象上处于等待的所有线程;
  3. wait,让当前线程进入等待状态,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者是设置了超时时间,等待超时时间结束。该方法有多个重载方法:
public final void wait() throws InterruptedException
// 超时时间如果是0,则不考虑超时时间,只能等待被唤醒
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException

  不过需要注意的是,wait和notify方法在使用的时候都必须要先持有对象的锁,如果没有同步措施,对象有可能出现不确定的锁的状态。因为同一时刻,一个对象只能被一个线程占有,调用wait方法就表示将持有对象锁的线程释放锁,释放对应的CPU资源,然后进入等待状态。notify类似,如果线程在调用notify的时候没有获取到锁,那么notify有可能将其他处于waiting状态的线程唤醒,所以调用这两个方法的时候,必须先获得该对象的锁。

有关notify的API说明,可以简单看一下:

This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:

  • By executing a synchronized instance method of that object.
  • By executing the body of a synchronized statement that synchronizes on the object.
  • For objects of type Class, by executing a synchronized static method of that class.

Only one thread at a time can own an object's monitor.

而有关notify和notifyAll方法的区别,可以参考:java中的notify和notifyAll有什么区别?

本文主要参考自:
海子-Java并发编程:Thread类的使用
占小狼-JVM源码分析之Object.wait/notify实现
《Java并发编程实战》

上一篇 下一篇

猜你喜欢

热点阅读