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

一. 前言

  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方法
    1. isInterrupted方法,前面已经讲过,用于修改线程中断状态的方法;
public boolean isInterrupted()
    1. isAlive方法,判断线程是否存活,也就是是否已经启动但没有死掉;
public final native boolean isAlive();
    1. setPrioritygetPriority,设置和获取线程的优先级;
public final void setPriority(int newPriority)
public final int getPriority() 
    1. setNamegetName,获取线程的名称;
public final synchronized void setName(String name)
public final String getName()
    1. getThreadGroup,获取线程所在的线程组,如果线程死掉了,那么返回null;
public final ThreadGroup getThreadGroup()
    1. setDaemonisDaemon,设置线程为守护线程以及判断线程是不是守护线程;
public final void setDaemon(boolean on)
public final boolean isDaemon()
    1. getStackTrace,获取当前线程的堆栈跟踪信息,返回一个StackTraceElement数组:
public StackTraceElement[] getStackTrace()
    1. getAllStackTraces,获取所有活动线程的堆栈跟踪映射:
public static Map<Thread, StackTraceElement[]> getAllStackTraces()

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

Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread thread : threadSet) {
    System.out.println(thread.getName());
}
    1. getId,获取该线程的标识符,线程id是唯一的,在其生命周期内保持不变:
public long getId() {
    return tid;
}
    1. getState,获取线程状态,前面已经了解过,线程状态对应于Thread的内部枚举类型State:
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并发编程实战》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容

  • 单任务 单任务的特点是排队执行,也就是同步,就像再cmd输入一条命令后,必须等待这条命令执行完才可以执行下一条命令...
    Steven1997阅读 1,126评论 0 6
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,783评论 3 53
  • 感恩家族给我的功课,让我一步一步找回自己。 感恩孩子们的到来,让我的坏种子提前,对不起,谢谢你,我爱你 原谅我。 ...
    疗愈师李玉阅读 229评论 0 2
  • 1. 今晚,是和朋友阿东喝的最后一杯酒了,因为他马上要辞职了,离开这个城市。 当年到公司的他,任劳任怨,同事没吃早...
    jingyueyan阅读 743评论 0 3
  • 《搞定》 【横向整理任务】 1.罗列所有任务 钢琴 简书剽悍晨读 赖世雄 洗t恤 教弟弟说话 2.杂事分类 洗t恤...
    20000字阅读 135评论 3 2