Java多线程编程

java给多线程编程提供了内置的支持。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程 ,每条线程并行执行不同的任务。

一个进程包括由操作系统分配的内存空间,包括一个或多个线程。一个线程不能独立的存在,他必须是进程的一部分。一个进程一直运行,直到所有的非收回线程都结束运行后才能结束。

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

image-20210427163338918

  • 新建状态

    使用new关键字和Thread类或器子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()线程。

  • 就绪状态

    当线程对象调用了start()方法之后,该线程就进入了就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态

    如果就绪状态的线程以获取CPU资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,他可以变为阻塞状态就绪状态死亡状态

  • 阻塞状态

    如果一个线程执行了sleep(睡眠)suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态

    一个运行状态的线程完成任务或者其他终止条发生时,该线程就切换到终止状态

线程的优先级

每一个java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

java线程的优先级是一个整数,其取值范围 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

Java 中多线程的实现

主要有三种方案:

  • Thread class 继承Thread类

  • Runnable接口 实现Runnable接口

  • Callable接口 实现Callable接口

Thread类

  • 继承Thread类, 并重写run() 方法
  • Thread类底层也是实现Runnable接口,并重写了run()方法
  • 调用start()方法后,JVM为创建一个新的线程,并将该线程设置为可运行态Runnable,但并没有直接运行
  • 当线程第一次得到时间片时,run() 方法得以运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class TestThread {
public static void main(String args[]){
ThreadDemo T1 = new ThreadDemo("Thread-1");
T1.start();

ThreadDemo T2 = new ThreadDemo("Thread-2");
T2.start();

for(int i = 200000; i > 0; i--){
System.out.println("Main: " + i);
}
}

}


class ThreadDemo extends Thread{
private Thread t;
private String threadName;
ThreadDemo(String name) {
threadName = name;
System.out.println("Creating" + threadName);
}
public void run(){
System.out.println("Running" + threadName);
try {
for(int i = 4; i > 0; i--){
System.out.println("Thread: " + threadName + " interrupted.");
// 让线程睡眠一会
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread: " + threadName + "exiting.");
}

public void start(){
System.out.println("Starting " + threadName);
if(t == null){
t = new Thread(this, threadName);
t.start();
}
}
}

start() 方法不可被多次调用,否则会抛出异常。

Thread类的常用方法

方法 描述
public void start() 开始线程,将新线程设置为就绪态; Java虚拟机调用该线程的run()方法
public void run() 线程获得时间片时,自动被异步调用,真正开始执行该线程; 该线程实现自Runnable接口
public final void setName(String name) 改变线程名称,使之与参数name相同
public final void setPriority(int priority) 更改线程的优先级
public final void setDaemon(boolean on) 将该线程标记为收回线程或用户线程
public final void join(long millisec) 让当前线程等待另一个线程执行完毕后再继续执行; 底层是使用的Object类的wait()方法
public void interrupt() 中断线程
public final boolean isAlive() 判断线程是否处于活动状态

上述方法是被Thread对象调用的,线面的方法是Thread类的静态方法。

方法 描述
public static void yield() 让出当前的CPU时间片,并重现变成就绪状态,重新竞争CPU。让出后,可能当前CPU使用权还会被该线程获取到。
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

线程sleep和yield的区别

  • sleep()方法暂停当前线程后,会给其他线程执行机会,线程优先级对此没有影响。yield()方法会给优先级相同或更高的线程更高的执行机会
  • sleep()方法会将线程转入阻塞状态,知道阻塞时间结束,才会转入就绪状态。yield()方法会将当前线程直接转入就绪状态。
  • sleep()方法生命抛出InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显示声明抛出该异常。yield()方法则没有声明抛出任何异常。
  • sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行。

实现Runnable接口

如果一个子类已经继承了一个类,就无法在继承Thread类,此时可以通过Runnable接口创建线程。

  • 通过实现Runnable接口创建RunnableDemo线程,对其进行实例化

  • 创建Thread类的实例并传入RunnableDemo线程的实例

  • 调用线程的start方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class TestRunnable {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo( "Thread-1");
R1.start();

RunnableDemo R2 = new RunnableDemo( "Thread-2");
R2.start();
}
}
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;

RunnableDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}

public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}

public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}

通过 Runnable 接口来创建并启动线程,有两种方式:

  1. 通过 普通类 的方式或 函数式编程匿名类 的方式来创建 Runnable 接口的实现类,并实现 run() 方法。
  2. 传入 Runnable 的实现类,实例化 Thread 类对象。
  3. 调用 start() 方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}

public static void main(String[] args) {
// 通过 Thread 类来创建新线程
new Thread(new MyThread()).start();

// Java 8 函数式编程,创建匿名类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}

通过 Callable 和 ExecutorService创建线程

有时,我们需要在主线程中开启多个子线程并发执行一个任务,然后收集各个线程执行返回的结果并将最终结果汇总起来,这时就要用到Callable接口。

  • 创建callable接口的实现类,并实现call方法,该call()方法将作为线程执行体,并且有返回值。

  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该Future List对象封装了该Callable对象的call()方法的返回值

  • 使用Future List 对象作为Thread对象的target创建并启动新线程

  • 调用Future List对象的get()方法来获得子线程执行结束后的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TestCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// step 2: 创建一个固定大小为5的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// step 3: 创建有多个有返回值的任务列表
List<Future> list = new ArrayList<Future>();
for(int i = 0; i < 5; ++i){
// step 4: 创建一个有返回值的线程实例
Callable c = new MyCallable(i + " ");
// step 5: 提交线程,获取Future对象并将其保存到Future List中
Future future = pool.submit(c);
System.out.println("submit a callable thread: " + i);
list.add(future);
}

// step 6: 关闭线程池,等待线程执行结束
pool.shutdown();
// step 7: 遍历所有线程的运行结果,
for (Future future : list){
// 从Future 对象上获取任务的返回值 并将结果输出到控制台
System.out.println("get the result from callable thread: " + future.get().toString());
}

}

}

// step1 :通过实现Callable接口创建MyCallable线程
class MyCallable implements Callable<String>{
private String name;
// 通过构造函数为线程传递函数,以定义线程的名称
public MyCallable (String name){
this.name = name;
}

@Override
public String call() throws Exception {
return name;
}

}

创建线程的三种方式的对比

  • 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
  • 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

面试: 线程无线次创建的时候会拖垮CPU么 ?

不会拓宽CPU, 只会占满内存,造成out of memery