Java线程的创建与使用

78 阅读13分钟

在阅读完《java并发编程的艺术》以及经历过一段时间的实习之后,对java线程的了解和使用有了更深的理解。为了巩固印象,方便日后复习,决定写一篇文章记录一下这段时间的收获。

1.进程与线程的概念和他们之间的关系

工欲善其事必先利其器,要想学好Java并发编程,不熟悉进程和线程的概念怎么能行? 虽然面试很少问,但笔试中还是经常出现的。

什么是进程?

进程是系统进行资源调度和分配的最小单位
进程是计算机中运行的程序的实例。它是操作系统对一个正在执行的程序的抽象表示。每个进程都有自己的内存空间,
包含代码、数据和堆栈等信息。

什么是线程

线程是CPU调度和分派的最小单位
线程是进程中的一个执行单元,一个进程里可以创建多个线程。这些线程都拥有各自的计数器,堆栈和局部变量等属性,
并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感到这些线程在同时执行。

为什么要使用多线程

1.更多的处理器核心

一个线程只能运行在一个处理器核心上,一个单线程程序在运行时只能使用一个处理器核心。那么再多的处理器核心也无法显著提升该程序的执行效率。
如果使用多线程,将计算逻辑分配到多个处理器核心上,就会显著减少程序处理时间。

 以下代码是ConcurrentHashMap源码中transfer方法的一部分,作用是根据系统的CPU核心数和哈希表的大小,动态地确定一个合适的步长。
 通过将数据迁移任务平均分配给多个线程,并保证每个线程处理的元素数量不低于最小迁移步长,可以提高数据迁移的效率和并行度。
       if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; 
        if (nextTab == null) {          
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }

2.更快地响应时间

在业务逻辑复杂的情况下,我们可以将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列)。缩短响应时间,提升用户体验。

3.更好的编程模型

Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,
而不是绞尽脑汁地考虑如何将其多线程化。一旦开发人员建立好了模型,稍做修改总是能够方便地映射到Java提供的多线程编程模型上。

线程的生命周期

Java线程的生命周期可以分为5个状态,分别是新建(NEW)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、和终止(Terminated)。
下面将对每个状态进行详细介绍
1.新建(NEW):当通过Thread类的实例或者实现Runnable接口并传递给Thread类的构造器来创建一个线程时,线程处于新建状态。
此时线程已经被创建,但还没有启动。new状态通过调用start方法进入runnable状态
2.就绪(Runnable):当调用线程对象的start方法后,线程会进入就绪状态。
此时线程已获得除CPU资源外的所有资源,等待系统调度器分配CPU时间片来执行线程。
就绪状态中多个线程之间存在竞争关系。哪个线程被优先执行由系统调度器决定。
由于存在 Running 状态,所以不会直接进入 Blocked 状态和 Terminated 状态,即使是在线程的执行逻辑中调用 wait、sleep 或者其他的 block 的 IO 操作等,
也必须先获得CPU 的调度执行权才可以,严格来讲,Runnable 的线程只能意外终止或者进入 Running状态
3.运行(Running):当系统调度器将CPU时间片分配给就绪状态的线程时,线程开始执行其run()方法中的代码,处于运行状态。
在该状态中,线程的状态可以发生如下的状态转换。
    a. 直接进入 Terminated 状态,比如调用 JDK 已经不推荐使用的 stop 方法或者意外死亡;
    b. 进入 Blocked 状态,比如调用 sleep 或者 wait 方法而加入了 waitSet 中;
    c. 进行某个阻塞的 IO 操作,比如因网络数据的读写而进入了 Blocked;
    d. 获取某个锁资源,从而加入到该锁的阻塞队列中而进入了 Blocked;
    e. 由于 CPU 的调度器轮询使该线程放弃执行,进入 Runnable 状态;
    f. 线程主动调用 yield 方法,放弃 CPU 执行权,进入 Runnable;
4.阻塞(Blocked):当线程进入阻塞状态后,它会放弃CPU资源,处于不活动状态,直到阻塞的原因消失。线程在Blocked状态可以切换至如下几个状态。
    a. 直接进入 Terminated 状态,比如调用 JDK 已经不推荐使用的 stop 方法或者意外死亡;
    b. 线程阻塞的操作结束,比如读取了想要的数据字节进入到 Runnable;
    c. 线程完成了指定时间的休眠,进入到了 Runnable;
    d. wait 中的线程被其他线程 notify/notifyall 唤醒,进入 Runnable;
    e. 线程获取到了某个锁资源,进入 Runnable;
    f. 线程在阻塞过程中被打断,比如其他线程调用了 interrupt 方法,进入 Runnable。
4.终止(Terminated):
Terminated 是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入 Terminated,意味着该线程的生命周期都结束了,
下面这些情况会使线程进入Terminated 状态。
    a. 线程运行正常结束,结束生命周期;
    b. 线程运行出错,意外结束;
    c. JVM Crash,导致所有的线程都结束。

java中创建线程的方式

1.通过继承Thread类创建线程

class MyThread extends Thread {
        @Override public void run() { 
        // 线程执行的代码逻辑 
        System.out.println("Thread created by extending Thread class."); 
        } 
     }

2.通过实现Runnable接口创建线程:

class MyRunnable implements Runnable { 
        @Override 
       public void run() { 
       // 线程执行的代码逻辑 
       System.out.println("Thread created by implementing Runnable interface.");
       } 
     }

测试代码

public class Main {
      public static void main(String[] args) { 
      // 创建并启动线程 
      Thread thread = new Thread(new MyRunnable()); 
      thread.start();
      // 创建并启动线程 
       MyThread thread = new MyThread(); 
       thread.start();
         } 
      }

线程start源码分析

public synchronized void start (){
        if(threadStatus!=0)//状态校验 0表示NEW状态
            throw new IllegalThreadStateException () ;
        group.add(this);//添加进线程组
        boolean started = false;
        try{
            start0();//调用native方法执行线程run方法
            started = true;
        }finally{
            try{
                if(!started){
                    group.threadStartFailed(this);//启动失败,从线程组中移除当前线程
                }
            } catch(Throwable ignore){
            }
        }
    }
    private native void start0();//native方法,C++程序执行
仔细阅读源码会总结出以下几点:
    a. Thread 被构造后的 NEW 状态,threadStatus 这个内部属性为 0;
    b. 不能两次启动 Thread,否则就会出现IllegalThreadStateException 异常;
    c. 线程启动后将会被加入到一个 ThreadGroup 中
    d. 一个线程生命周期结束,也就是到了 Terminated 状态,再次调用 start 方法是非法的,
    也就是说 Terminated 状态是没有办法回到 Runnable/Running 状态的。
线程真正的执行逻辑是在 run 方法中,而启动线程的却是 start 方法,这是因为用了模板方法设计模式。

ThreadAPI的详细介绍

sleep方法介绍

sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行。休眠时不放弃monitor锁的所有权。
public class ThreadSleep {
public static void main(String[] args) {
// 子线程
new Thread(() -> {
long startTime = System.currentTimeMillis();
sleep(2000L);
long endTime = System.currentTimeMillis();
System.out.printf("Total spend %d ms\n", endTime - startTime);
}).start();
// main 线程
long startTime = System.currentTimeMillis();
sleep(3000L);
long endTime = System.currentTimeMillis();
System.out.printf("Main thread spend %d ms", endTime - startTime);
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
    e.printStackTrace();
}
}
}

yield方法介绍

yield是一种启发式的方法,会提醒调度器放弃当前线程的CPU资源,如果CPU资源不紧张,则会忽略这种提醒。
public class ThreadYield {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
IntStream.range(0,
2).mapToObj(ThreadYield::create).forEach(Thread::start);
System.out.println("---------- 华丽分割线----------");
TimeUnit.MILLISECONDS.sleep(500L);
}
}
private static Thread create(int index) {
return new Thread(() -> {
if (index == 0)
Thread.yield();
System.out.println(index);
});
}
}

设置线程优先级

进程有进程的优先级,线程同样也有优先级,理论上是优先级比较高的线程会获取优先
被 CPU 调度的机会,但是事实上往往并不会如你所愿,设置线程的优先级同样也是一个 hint操作,具体如下。
对于 root 用户,他会 hint 操作系统你想要设置的优先级别,否则它会被忽略。
如果 CPU 比较忙,设置优先级可能会获得更多的 CPU 时间片,但是闲时优先级的高低几乎不会有任何作用。
线程的优先级不能小于 1 也不能大于 10,如果指定的线程优先级大于线程所在 group 的优先级,
那么指定的优先级将会失败,取而代之的是 group 的最大优先级
线程默认的优先级和它的父类保持一致,一般情况下都是 5,因为 main 线程的优先级就是 5
public class ThreadPriority {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
System.out.println("t1");
}
});
t1.setPriority(3);
Thread t2 = new Thread(() -> {
53
while (true) {
System.out.println("t2");
}
});
t2.setPriority(10);
t1.start();
t2.start();
}
}

获取线程ID与当前线程

public long getId() 获取线程的唯一 ID,线程的 ID 在整个 JVM 进程中都会是唯一的,并且是从 0 开始逐次递增
 public class CurrentThread {
     public static void main(String[] args) {
         Thread t1 = new Thread(){
             @Override
         public void run() {
             System.out.println(Thread.currentThread() == this);
             System.out.println(Thread.currentThread().getId());
             }
         };
         t1.start();
         String name = Thread.currentThread().getName();
         System.out.println("main".equals(name));
     }
 }

Interrupt

以下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt 方法,就可以打断阻塞。
Object 的 wait 方法;
Objectsleep(long)方法
Thread 的 sleep(long)方法
Thread 的 join 方法
InterruptIbleChannel 的 io 操作
Selector 的 wakeup 方法
述若干方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的
interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterruptedException 的异常,
这个异常就像一个 signal(信号)一样通知当前线程被打断了
如果一个线程已经是死亡状态,那么尝试对其的 interrupt 会直接被忽略
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
System.out.println(" 阻塞状态被中断");
e.printStackTrace();
}
});
t1.start();
// 短暂的阻塞是为了保证 t1 线程已启动
TimeUnit.MILLISECONDS.sleep(100);
// 中断 t1 线程的阻塞状态
t1.interrupt();
}
} 

isInterrupted

isInterrupted 是 Thread 的一个成员方法,它主要判断当前线程是否被中断,
该方法仅仅是对 interrupt 标识的一个判断,并不会影响标识发生任何改变
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
}
}
};
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
System.out.printf("Thread is interrupted ? %s\n",
t1.isInterrupted());
t1.interrupt();
System.out.printf("Thread is interrupted ? %s\n",
t1.isInterrupted());
}
}

interrupted

interrupted 是一个静态方法,虽然其也用于判断当前线程是否被中断,但是它和成员方法 isInterrupted 还是有很大的区别,
调用该方法会直接擦除掉线程的 interrupt标识,需要注意的是,如果当前线程被打断了,那么第一次调用 interrupted 方法会返回true,
并且立即擦除了 interrupt 标识;第二次包括以后的调用永远都会返回 false,除非在此期间线程又一次被打断
public class ThreadInterrupt {
59
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
System.out.println(Thread.interrupted());
}
}
};
t1.setDaemon(true);
t1.start();
// 短暂的阻塞是为了保证 t1 线程已启动
TimeUnit.MILLISECONDS.sleep(2);
// 中断 t1 线程的阻塞状态
t1.interrupt();
}
}
 interrupted 方法和 isInterrupted 方法都调用了同一个本地方法
 
private native boolean isInterrupted(boolean ClearInterrupted);
其中参数 ClearInterrupted 主要用来控制是否擦除线程 interrupt 的标识。
isInterrupted 方法的源码中该参数为 false,表示不想擦除:
public boolean isInterrupted() {
return isInterrupted(false);
}
而 interrupted 静态方法中该参数则为 true,表示想要擦除:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}

线程 join

join 某个线程 A,会使当前线程 B 进入等待,直到线程 A 结束生命周期,
或者到达给定的时间,那么在此期间 B 线程是处于 Blocked 的,而不是 A 线程
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
// 1. 定义两个线程
Thread t1 = new Thread(() -> printNum());
Thread t2 = new Thread(() -> printNum());
// 2. 启动这两个线程
t1.start();
t2.start();
// 3. 执行这两个线程的 join 方法
t1.join();
t2.join();
// 4. main 线程循环输出
printNum();
}
private static void printNum() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "#" + i);
}
}
}

如何关闭一个线程

JDK 有一个 Deprecated 方法 stop,但是该方法存在一个问题,JDK 官方早已经不推荐使用,其在后面的版本中有可能会被移除。
根据官网的描述,该方法在关闭线程时可能不会释放掉 monitor 的锁,所以强烈建议不要使用该方法结束线程,下面将介绍几种关闭线程的方法。

捕获中断信号关闭线程

我们通过 new Thread 的方式创建线程,这种方式看似很简单,其实它的派生成本是比较高的,
因此在一个线程中往往会循环地执行某个任务,比如心跳检査,不断地接收网络消息报文等,系统决定退出的时候,可以借助中断线程的方式使其退出。
public class InterruptThreadExit {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("I will start work.");
while (!isInterrupted()) {
// working.
}
System.out.println("I will be exiting.");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("System will be shutdown.");
t.interrupt();
}
}

使用 volatile 开关控制

由于线程的 interrupt 标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方法,所以使用 volatile 修饰的开关 flag 关闭线程也是一种常用的做法
public class FlagThreadExit {
public static void main(String[] args) throws InterruptedException {
MyTask t = new MyTask();
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("System will be shutdown.");
67
t.close();
}
}
class MyTask extends Thread {
private volatile boolean closed = false;
@Override
public void run() {
System.out.println("I will start work.");
while (!closed && !isInterrupted()) {
// working.
}
System.out.println("I will be exiting.");
}
public void close() {
this.closed = true;
this.interrupt();
}
}

异常退出

在一个线程的执行单元中,是不允许抛出 checked 异常的,不论 Thread 中的 run 方法,还是 Runnable 中的 run 方法,
如果线程在运行过程中需要捕获 checked 异常并且判断是否还有运行下去的必要,那么此时可以将 checked 异常封装成 unchecked 异(RuntimeException)抛出进而结束线程的生命周期

有时间再整理下线程安全的知识。一点一滴的积累吧