Java 供给了多线程编程的内置支持,让咱们能够轻松开发多线程应用。

Java 中咱们最为了解的线程便是 main 线程——主线程。

一个进程能够并发多个线程,每条线程并行履行不同的使命。线程是进程的基本单位,是一个单一次序的操控流,一个进程一向运转,直到一切的“非看护线程”都完毕运转后才能完毕。Java 中常见的看护线程有:废物收回线程、

这儿简要讲述以下并发和并行的差异。

并发:同一时刻段内有多个使命在运转

并行:同一时刻点上有多个使命一同在运转

多线程能够协助咱们高效地履行使命,合理运用 CPU 资源,充分地发挥多核 CPU 的功能。可是多线程也并不总是能够让程序高效运转的,多线程切换带来的开支、线程死锁、线程反常等等问题,都会使得多线程开发较单线程开发更费事。因而,有必要学习 Java 多线程的相关常识,然后提高开发效率。

1 创立多线程

依据官方文档Thread (Java Platform SE 8 ) (oracle.com)中java.lang.Thread的阐明,能够看到线程的创立办法主要有两种:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

能够看到,有两种创立线程的办法:

  • 声明一个类承继Thread类,这个子类需求重写run办法,随后创立这个子类的实例,这个实例就能够创立并发动一个线程履行使命;
  • 声明一个类完成接口Runnable并完成run办法。这个类的实例作为参数分配给一个Thread实例,随后运用Thread实例创立并发动线程即可

除此之外的创立线程的办法,诸如运用Callable和FutureTask、线程池等等,无非是在此基础上的扩展,查看源码能够看到FutureTask也完成了Runnable接口。

运用承继 Thread 类的办法创立线程的代码:

/**
 * 运用承继 Thread 类的办法创立线程
 */ public class CreateOne { public static void main(String[] args) {
        Thread t = new MySubThread();
        t.start();
    }
} class MySubThread extends Thread { @Override public void run() { // currentThread() 是 Thread 的静态办法,能够获取正在履行当时代码的线程实例 System.out.println(Thread.currentThread().getName() + "履行使命");
    }
} // ================================== 运转成果 Thread-0履行使命

运用完成 Runnable 接口的办法创立线程的代码:

/**
 * 运用完成 Runnable 接口的办法创立线程
 */ public class CreateTwo { public static void main(String[] args) {
        RunnableImpl r = new RunnableImpl();
        Thread t = new Thread(r);
        t.start();
    }
} class RunnableImpl implements Runnable { @Override public void run() {
        System.out.println(Thread.currentThread().getName() + "履行使命");
    }
} // ================================== 运转成果 Thread-0履行使命

1.1 孰优孰劣

创立线程虽然有两种办法,可是在实践开发中,运用完成接口Runnable的办法更好,原因如下:

  1. 查看Thread的run办法,能够看到:
// Thread 实例的成员变量 target 是一个 Runnable 实例,能够经过 Thread 的结构办法传入 private Runnable target; // 假如有传入 Runnable 实例,那么就履行它的 run 办法 // 假如重写,就彻底履行咱们自己的逻辑 public void run() { if (target != null) {
        target.run();
    }
}
  1. 查看上面的源码,咱们能够知道,Thread 类并不是界说履行使命的主体,而是Runnable界说履行使命内容,Thread调用履行,然后完成线程与使命的解耦。
  2. 因为线程与使命解耦,咱们能够复用线程,而不是当需求履行使命就去创立线程、履行完毕就毁掉线程,这样带来的系统开支太大。这也是线程池的基本思想。
  3. 此外,Java 只只支持单承继,假如承继 Thread 运用多线程,那么后续需求经过承继的办法扩展功能,那会适当费事。

2 start 和 run 办法

从上面能够得知,有两种创立线程的办法,咱们经过Thread类或Runnable接口的run办法界说使命,经过Thread的start办法创立并发动线程。

❗❗ 咱们不能经过run办法发动并创立一个线程,它仅仅一个普通办法,假如直接调用这个办法,其实仅仅调用这个办法的线程在履行使命算了。

// 将上面的代码修正一下,查看履行成果 public class CreateOne { public static void main(String[] args) {
        Thread t = new MySubThread();
        t.run(); //t.start(); }
} // ===================== 履行成果 main履行使命

查看 start 办法的源码:

// 线程状况,为 0 表示还未发动 private volatile int threadStatus = 0; // ❗❗ 同步办法,确保创立、发动线程是线程安全的 public synchronized void start() { // 假如线程状况不为 0,那么抛出反常——即线程现已创立了 if (threadStatus != 0) throw new IllegalThreadStateException(); // 将当时线程添加到线程组 group.add(this); boolean started = false; try { // 这是一个本地办法 start0();
        started = true;
    } finally { try { if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
} // 由本地办法完成,只需求知道,该办法调用后会创立一个线程,而且会履行 run 办法 private native void start0();

由上面的源码能够得知:

  1. 创立并发动一个线程是线程安全的
  2. start() 办法不能重复调用,否则会抛出反常

3 怎样中止线程

线程并不是无休止地履行下去的,一般状况下,线程中止的条件有:

  1. run 办法履行完毕
  2. 线程产生反常,可是没有捕获处理

除此之外,咱们还需求自界说某些状况下需求告诉线程中止,例如:

  1. 用户主动取消使命
  2. 使命履行时刻超时、犯错
  3. 呈现毛病,服务需求快速中止

为什么不能直接简略粗犷的中止线程呢?经过告诉线程中止使命,咱们能够更高雅地中止线程,让线程保存问题现场、记载日志、发送警报、友爱提示等等,令线程在适宜的代码方位中止线程,然后防止一些数据丢失等状况。

令线程中止的办法是让线程捕获中止反常或检测中止标志位,然后高雅地中止线程,这是引荐的做法。而不引荐的做法有,运用被符号为过期的办法:stop,resume,suspend,这些办法可能会造成死锁、线程不安全等状况,因为现已过期了,所以不做过多介绍。

3.1 告诉线程中止

咱们要运用告诉的办法中止方针线程,经过以下办法,期望能够协助你掌握中止线程的办法:

/**
 * 中止线程
 */ public class InterruptThread { public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> { long i = 0; // isInterrupted() 检测当时线程是否处于中止状况 while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println(i);
        });
        t.start(); // 主线程睡觉 1 秒,告诉线程中止 Thread.sleep(1000);
        t.interrupt();
    }
} // 运转成果 1436125519 

这是中止线程的办法之一,还有其他办法,当线程处于堵塞状况时,线程并不能运转到检测线程状况的代码方位,然后正确呼应中止,这个时候,咱们需求经过捕获反常的办法中止线程:

/**
 * 经过捕获中止反常中止线程
 */ public class InterruptThreadByException { public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{ long i = 0; while (i < Long.MAX_VALUE) {
                i++; try { // 线程大部分时刻处于堵塞状况,sleep 方***抛出中止反常 InterruptedException Thread.sleep(100);
                } catch (InterruptedException e) { // 捕获到中止反常,代表线程被告诉中止,做出相应处理再中止线程 System.out.println("线程收到中止告诉   " + i); // 假如 try-catchwhile 代码块之外,能够不用 return 也能够完毕代码 // 在 while 代码块之内,假如没有 return / break,那么仍是会进入下一次循环,并不能正确中止 return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
} // 运转成果 线程收到中止告诉 10 

以上,便是中止线程的正确做法,此外,捕获中止反常后,会铲除线程的中止状况,在实践开发中需求特别注意。例如,修正上面的代码:

public class InterruptThreadByException { public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{ long i = 0; while (i < Long.MAX_VALUE) {
                i++; try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("线程收到中止告诉   " + i); // ❗❗ 添加这行代码,捕获到中止反常后,检测中止状况,中止状况为 false System.out.println(Thread.currentThread().isInterrupted()); return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}

所以,在线程中,假如调用了其他办法,假如该办法有反常产生,那么:

  1. 将反常抛出,而不是在子办法内部捕获处理,由 run 办法一致处理反常
  2. 捕获反常,并从头告诉当时线程中止,Thread.currentThread().interrupt()

例如:

public class SubMethodException { public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ExceptionRunnableA());
        Thread t2 = new Thread(new ExceptionRunnableB());
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.interrupt();
        t2.interrupt();
    }
} class ExceptionRunnableA implements Runnable { @Override public void run() { try { while (true) {
                method();
            }
        } catch (InterruptedException e) {
            System.out.println("run 办法内部捕获中止反常");
        }
    } public void method() throws InterruptedException {
        Thread.sleep(100000L);
    }
} class ExceptionRunnableB implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) {
            method();
        }
    } public void method() { try {
            Thread.sleep(100000L);
        } catch (InterruptedException e) {
            System.out.println("子办法内部捕获中止反常"); // 假如不从头设置中止,线程将不能正确呼应中止 Thread.currentThread().interrupt();
        }
    }
}

综上,总结出令线程正确中止的办法为:

  1. 运用interrupt()办法告诉方针线程中止,符号方针线程的中止状况为true

  2. 方针线程经过isInterrupted()不时地检测线程的中止状况,依据状况决议是否中止线程

  3. 假如线程运用了堵塞办法例如sleep(),那么需求捕获中止反常并处理中止告诉,捕获了中止反常会重置中止符号位

  4. 假如run()办法调用了其他子办法,那么子办法:

    1. 将反常抛出,传递到顶层run办法,由run办法一致处理
    2. 将反常捕获,一同从头告诉当时线程中止

下面再说说关于中止的几个相关办法和一些会抛出中止反常的办法,运用的时候需求特别注意。

3.2 线程中止的相关办法

  1. interrupt()实例办法,告诉方针线程中止。
  2. static interrupted()静态办法,获取当时线程是否处于中止状况,会重置中止状况,即假如中止状况为true,那么调用后中止状况为false。办法内部经过Thread.currentThread()获取履行线程实例。
  3. isInterrupted()实例办法,获取线程的中止状况,不会铲除中止状况。

3.3 堵塞并能呼应中止的办法

  • Object.wait()
  • Thread.sleep()
  • Thread.join()
  • BlockingQueue.take() / put()
  • Lock.lockInterruptibly()
  • CountDownLatch.await()
  • CyclicBarrier.await()
  • Exchanger.exchange()

4 线程的生命周期

线程的生命周期状况由六部分组成:

状况 阐明
NEW 线程刚创立,还没有调用start办法,线程尚未发动
RUNNABLE 线程现已调用了start办法,现已准备好运转,正在等候 CPU 分配资源;或许正在运转
BLOCKED 进入synchronized代码块,可是没有拿到方针锁,进入堵塞状况
WAITING synchronized代码块中,同步锁方针调用了wait办法,或许线程被调用了join办法等,进入等候状况,需求被唤醒
TIMED WAITING 计时等候状况,等候一段时刻主动复苏,或许等候进程中被唤醒
TERMINATED 线程履行完毕,正常完毕或许产生未捕获的反常

能够用一张图总结线程的生命周期,以及各个进程之间是怎么转换的:

Java多线程

5 Thread 和 Object 中的线程办法

现在,咱们现已知道了线程的创立、发动、中止以及线程的生命周期了,那么,再来看看线程相关的办法有哪些。

首先,看看Thread中的一些办法:

办法 阐明
sleep() 让线程等候一段时刻,不会开释锁
join() 当时线程等候方针线程运转完毕
yield() 放弃现已取得的 CPU 资源,线程依旧是 RUNNABLE 状况
currentThread() 获取当时的线程实例
start() 发动线程
run() 线程使命主体
interrupt() 告诉线程中止,设置中止标志为true
isInterrupted() 查看线程是否处于中止状况
interrupted() 回来线程中止状况,会重置线程中止状况
stop()/suspend()/resume() 过期的中止线程办法

再看看Object中的相关办法:

办法 阐明
wait() 线程获取方针锁,进入等候状况,有必要合作同步代码块运用;
要么被唤醒,要么计时等候时刻完毕
notify() 随机唤醒一个线程,被唤醒线程测验获取同步锁
notifyAll() 唤醒一切线程,一切线程都会测验获取同步锁

运转以下代码,查看wait()和sleep()是否会开释同步锁

/**
* 证明 sleep 不会开释锁,wait 会开释锁
*/ public class SleepAndWait { private static Object lock = new Object(); public static void main(String[] args) {
        Thread t1 = new Thread(()->{ synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "取得同步锁,调用 wait() 办法"); try {
                    lock.wait(2000);
                    System.out.println(Thread.currentThread().getName() + "从头取得同步锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(()->{ synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "取得同步锁,唤醒另一个线程,调用 sleep()");
                lock.notify(); try { // 假如 sleep() 会开释锁,那么在此期间,上面的线程将会持续运转,即 sleep 不会开释同步锁 Thread.sleep(2000); // 假如履行 wait 办法,那么上面的线程将会持续履行,证明 wait 方***开释锁 //lock.wait(2000); System.out.println(Thread.currentThread().getName() + "sleep 完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

上面的代码现已证明了sleep()不会开释同步锁,此外,sleep()也不会开释Lock的锁,运转以下代码查看成果:

/**
 * sleep 不会开释 Lock 锁
 */ public class SleepDontReleaseLock implements Runnable { private static Lock lock = new ReentrantLock(); @Override public void run() { // 调用 lock 办法,线程会测验持有该锁方针,假如现已被其他线程锁住,那么当时线程会进入堵塞状况 lock.lock(); try {
            System.out.println(Thread.currentThread().getName() + "取得 lock 锁"); // 假如 sleep 会开释 Lock 锁,那么另一个线程会立刻打印上面的语句 Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "开释 lock 锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally { // 当时线程开释锁,让其他线程能够占有锁 lock.unlock();
        }
    } public static void main(String[] args) {
        SleepDontReleaseLock task = new SleepDontReleaseLock(); new Thread(task).start(); new Thread(task).start();
    }
}

5.1 wait 和 sleep 的异同

接下来总结Object.wait()和Thread.sleep()办法的异同点。

相同点:

  1. 都会使线程进入堵塞状况
  2. 都能够呼应中止

不同点:

  1. wait()是Object的实例办法,sleep()是Thread的静态办法
  2. sleep()需求指定时刻
  3. wait()会开释锁,sleep()不会开释锁,包括同步锁和Lock锁
  4. wait()有必要合作synchronized运用

6 线程的相关特点

现在咱们现已对 Java 中的多线程有必定的了解了,咱们再看看 Java 中线程Thread的一些相关特点,即它的成员变量。

特点 阐明
线程 ID 仅有标识线程,无法修正,从 1 递增,主线程 main ID 为 1
用户创立的线程 ID 并不是从 2 开端,虚拟机进程发动后还会创立其他线程,例如废物收回线程
名称 name 默许为Thread-自增ID
能够经过Thread.setName()自界说线程名,便利区别线程、排查问题
是否是看护线程 daemon false代表不为看护线程,一般会承继父线程的类型

为方针线程供给服务,非看护线程运转完毕后,会跟着虚拟机一同中止
一般不运用看护线程,用户线程一旦完毕,看护线程也会完毕 |
| 优先级 priority | 优先级从小到大为0-10, 默许为 5
一般不改动优先级,因为:

  1. 不同操作系统的优先级界说不同
  2. 优先级会被操作系统修正
  3. 低优先级的线程可能一致无法获取资源而无法运转 |

运转以下代码,了解线程的相关特点

public class ThreadFields { public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> { // 自界说线程的 ID 并不是从 2 开端 System.out.println("线程 " + Thread.currentThread().getName()
                               + " 的线程 ID " + Thread.currentThread().getId()); while (true) { // 看护线程一向运转,可是 用户线程即这儿的主线程完毕后,也会跟着虚拟机一同中止 }
        }); // 自界说线程名字 t.setName("自界说线程"); // 将其设置为看护线程 t.setDaemon(true); // 设置优先级 Thread.MIN_PRIORITY = 1     Thread.MAX_PRIORITY = 10 t.setPriority(Thread.MIN_PRIORITY);
        t.start(); // 主线程的 ID 为 1 System.out.println("线程 " + Thread.currentThread().getName() + " 的线程 ID " + Thread.currentThread().getId());
        Thread.sleep(3000);
    }
}

7 大局反常处理

在子线程中,假如产生了反常咱们能够及时捕获并处理,那么对程序运转并不会有什么恶劣影响。

可是,假如产生了一些未捕获的反常,在多线程状况下,这些反常打印出来的仓库信息,很容易淹没在庞大的日志中,咱们可能很难察觉到,而且不好排查问题。

假如对这些反常都做捕获处理,那么就会造成代码的冗余,编写起来也不便利。

因而,咱们能够编写一个大局反常处理器来处理子线程中抛出的反常,一致地处理,解耦代码。

7.1 源码查看

在解说怎么处理子线程的反常问题前,咱们先看看 JVM 默许状况下,是怎么处理未捕获的反常的。

查看 Thread 的源码:

public class Thread implements Runnable {
    【1】当产生未捕获的反常时,JVM 会调用该办法,并传递反常信息给反常处理器
        能够在这儿打下断点,在线程中抛出反常不捕获,IDEA 会跳转到这儿 // 向处理程序发送未捕获的反常。此办法仅由JVM调用。 private void dispatchUncaughtException(Throwable e) {2】查看第 9 行代码,能够看到假如没有指定反常处理器,默许是线程组作为反常处理器
        【3】调用这个反常处理器的处理办法,处理反常,查看第 15getUncaughtExceptionHandler().uncaughtException(this, e);
    } public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
    【4UncaughtExceptionHandlerThread 的内部接口,线程组也是该接口的完成,
        只要一个办法处理反常,接下来查看第 25 行,看看 Group 是怎么完成的 @FunctionalInterface public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e);
    }
} public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    【5】默许反常处理器的完成 public void uncaughtException(Thread t, Throwable e) { // 假如有父线程组,交给它处理 if (parent != null) {
            parent.uncaughtException(t, e);
        } else { // 获取默许的反常处理器,假如没有指定,那么为 null Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) {
                ueh.uncaughtException(t, e);
            } // 没有指定反常处理器,打印仓库信息 else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread "" + t.getName() + "" ");
                e.printStackTrace(System.err);
            }
        }
    }
}

7.2 自界说大局反常处理器

经过上面的源码解说,现已能够知道 JVM 是怎么处理未捕获的反常的了,即只打印仓库信息。那么,要怎么自界说反常处理器呢?

具体办法为:

  1. 完成接口Thread.UncaughtExceptionHandler并完成办法uncaughtException()
  2. 为创立的线程指定反常处理器

示例代码:

public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) {
        System.out.println("产生了未捕获的反常,进行日志处理、报警处理、友爱提示、数据备份等等......");
        e.printStackTrace();
    } public static void main(String[] args) {
        Thread t = new Thread(() -> { throw new RuntimeException();
        });
        t.setUncaughtExceptionHandler(new MyExceptionHandler());
        t.start();
    }
}

8 多线程带来的问题

合理地运用多线程能够带来功能上的提升,可是假如因为一些疏漏,多线程反而会成为程序员的噩梦。

例如,多线程开发,咱们需求考虑线程安全问题、功能问题。

首先,讲讲线程安全问题。

什么是线程安全?所谓线程安全,即

在多线程状况下,假如访问某个方针,不需求额外处理,例如加锁、令线程堵塞、额外的线程调度等,调用这个方针都能取得正确的成果,那么这个方针便是线程安全的

因而,在编写多线程程序时,就需求考虑某个数据是否是线程安全的,假如这个方针满意:

  1. 被多个线程共享
  2. 操作具有时序要求,先读后写
  3. 这个方针的类有他人编写,而且没有声明是线程安全的

那么咱们就需求考虑运用同步锁、Lock、并发工具类(java.util.concurrent)来确保这个方针是在多线程下是安全的。

再看看多线程带来的功能问题。

多个线程的调度需求上下文切换,这需求消耗 CPU 资源。

所谓上下文,即处理器中寄存器、程序计数器内的信息。

上下文切换,即 CPU 挂起一个线程,将其上下文保存到内存中,从内存中获取另一个运转线程的上下文,康复到寄存器中,依据程序计数器中的指令康复线程运转。

一个线程被挂起,另一个线程康复运转,这个时候,被挂起的线程的数据缓存对于运转线程来说是无效的,减缓了线程的运转速度,新的线程需求从头缓存数据提升运转速度。

一般状况下,密集的 IO 操作、抢锁操作都会带来密集的上下文切换。

以上,是上下文切换带来的功能问题,Java 的内存模型也会带来功能问题,为了确保数据的可见性,JVM 会强制令数据缓存失效,确保数据是实时最新的,这也献身了缓存带来的功能提升。

9 总结

这儿总结下上面的内容。

  1. 创立线程有两种办法,承继 Thread 和完成 Runnable

  2. start 办法才能正确创立和发动线程,run 办法仅仅一个普通办法

  3. start 办法不能重复调用,重复调用会抛出反常

  4. 正确中止线程的办法是经过interrupt()告诉线程

    1. 线程不时地查看中止状况并判别是否中止线程,运用办法isInterrupt()
    2. 假如线程堵塞,捕获中止反常,判别是否中止线程
    3. 线程调用的子办法最好将反常抛出,由 run 办法一致捕获处理
    4. 线程调用的子办法假如捕获反常,需求从头告诉线程中止
  5. 线程的生命周期为

    1. NEW
    2. RUNNABLE
    3. BLOCKED
    4. WAITING
    5. TIMED WAITING
    6. TERMINATED
  6. wait()/notify()/notifyAll() 有必要合作同步锁运用

  7. wait() 会开释锁,sleep() 不会开释锁,包括同步锁和 Lock 锁

  8. 线程的一些特点

    1. 线程ID,无法修正
    2. 线程名 name,能够自界说
    3. 看护线程 daemon,线程类型会承继自父线程,一般不指定线程为看护线程
    4. 优先级 priority,一般运用默许优先级,不改动优先级
  9. 能够自界说大局反常处理器,处理非主线程中的未捕获的反常,如备份数据、日志处理、报警等等

  10. 多线程开发会带来线程安全问题、功能问题,开发进程需求特别注意