Java线程笔记:线程入门

254 阅读4分钟

举个粟子

各种书籍与博客讲线程首先就是讲如何创建线程,因而就不再赘述了,直接用一个小例子开始:

示例1
public class StartThreads {
    public static void main(String[] args) {
    SubThread sub1 = new SubThread();
    sub1.start();
    System.out.println("main....");
    }
}

class SubThread extends Thread{
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("sub....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这个例子里,我们创建了一个简单的线程,并启动了它。其结果也容易想到,如下:

main.... sub....

在这个小例子中,我们让一个子线程“睡”了一秒再输出内容,随后整个程序才退出。如果将“睡眠”时间再加长点的话,肯定也是能得出相同的结果的。嗯?这似乎是对的呀,没有什么特殊的呀,程序就是要在所有线程执行完成之后再退出的呀。如果你跟我曾经观念相同的话,请看下面这个例子(如果不同的话,那么你应该猜到答案了):

示例2
public static void main(String[] args) {
    SubThread sub1 = new SubThread();
    sub1.setDaemon(true);
    sub1.start();
    System.out.println("main....");
}
// 其他代码同上

这里的输出如下

main.... Process finished with exit code 0

子线程里的输出语句没有被执行,也没有任何异常信息出现。

线程的种类--jvm视角

上面的例子表明,程序并不是在所有线程执行完成之后退出的。setDaemon方法注释中写道:The Java Virtual Machine exits when the only threads running are all daemon threads.(当惟一运行的线程都是守护进程线程时,Java虚拟机退出) 在Java里,线程可以分用户线程和守护线程。 用户线程就是平常我们创建的普通线程,它们会阻止程序的结束;而守护线程则不会阻止程序的结束。普通线程在调整start方法前使用类似sub1.setDaemon(true);的语句就可以把普通线程转换为守护线程(当然也是可以做相反的操作)。

事实上,守护线程这点并不什么大的知识点,这玩意一般也就是GC和一些框架的后台处理处理任务才会用到,平常是不可能用到的。不过了解这个知识点也是相当重要的,在以后的多线程调试过程才能不被一堆不知道从哪冒出来的线程吓住。

线程的“继承性”

线程之间是有“继承”的,将上面的例子稍微改造一下就可以看出来了:

示例3
public class StartThreads {
    public static void main(String[] args) throws InterruptedException {
        SubThread sub1 = new SubThread();
        sub1.setPriority(10);
        sub1.setDaemon(true);
        sub1.start();
        System.out.println("main....");
        Thread.sleep(42);
    }
}

class SubThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
            System.out.println("sub...." );
            new Thread(() -> System.out.println("sub of sub isDaemon "
                    + Thread.currentThread().isDaemon()
                    + " Priority:" + Thread.currentThread().getPriority())
            ).start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这里,我们在原先的子线程里再启动了一个匿名子线程,并用lambda表达式实现了Runnable接口(这里只是为了代码简洁点,并提醒下还没有学Java8语法的同学要抓紧了)。运行结果如下:

main.... sub.... sub of sub isDaemon true Priority:10

调整sub1.setPriority(10);sub1.setDaemon(true);语句里的参数,匿名子线程里的值也会跟着发生变化。这就是线程之间的“继承性”。


子子线程也为守护线程的情况下,主线程需要"睡眠"更多的时间来确保子子线程会运行。示例3中“睡眠”值是在我本地上恰好能保证子子线程能运行的阈值,在不同机子上会有不同的表现,具体原因不详

线程的生命周期

线程的生命周期,描述了一个线程由生到死的几个阶段。 网上搜索线程的生命周期,大概率是得到类似于下面的图:

线程的生命周期
这种图表现的是与语言无关的线程生命周期,是一种通用的模型(虽然写了很多很像是java的东西在上面)。 针对Java而言,线程的生命周期在Thread.State枚举类里就写得通俗易懂了。现整理如下:

  1. NEW:未开始。具体地讲,就是没有调用start()方法。
  2. RUNNABLE:已经在JVM里执行的线程,但它可能是等待操作系统的其他资源,如处理器
  3. BLOCKED:等待监视器锁(monitor lock)的状态,等待监视器锁去进入一个同步块或同步方法 (由synchronized关键字修饰的),或是在Object.wait()方法被调用之后重入一个同步块或同步方法。(此处指明了是监视器锁,那么它只与synchronized关键字修饰的代码相关,与Lock接口下的各种锁就没关系了)
  4. WAITING:等待状态。调用以下方法会产生此状态: a. Object.wait() b. Thread.join() c. LockSupport.park()
  5. TIMED_WAITING:指定时间的等待状态。以下方法调用产生此状态: a. Thread.sleep b. Object.wait(long) c. Thread.join(long) d. LockSupport.parkNanos e. LockSupport.parkUntil
  6. TERMINATED : 线程执行完成

注:没有标记()的方法说明是有重载方法符合情况

到此,线程的基础知识告一段落了。