什么是线程?

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

为什么使用多线程?

  1. 提高处理器核心的利用率(现代处理器核心越来越多,线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率。相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。)
  2. 提高响应时间(将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验)
  3. 更好的编程模型

    构造线程

    1. /**
    2. * Initializes a Thread.初始化一个线程
    3. *
    4. * @param g the Thread group
    5. * @param target the object whose run() method gets called
    6. * @param name the name of the new Thread
    7. * @param stackSize the desired stack size for the new thread, or
    8. * zero to indicate that this parameter is to be ignored.
    9. * @param acc the AccessControlContext to inherit, or
    10. * AccessController.getContext() if null
    11. * @param inheritThreadLocals if {@code true}, inherit initial values for
    12. * inheritable thread-locals from the constructing thread
    13. */
    14. private void init(ThreadGroup g, Runnable target, String name,
    15. long stackSize, AccessControlContext acc,
    16. boolean inheritThreadLocals) {
    17. if (name == null) {
    18. throw new NullPointerException("name cannot be null");
    19. }
    20. this.name = name;
    21. //当前线程为父线程
    22. Thread parent = currentThread();
    23. //用于返回安全管理器(如果存在),否则,如果安全管理器无法为当前应用程序建立,则返回null
    24. SecurityManager security = System.getSecurityManager();
    25. if (g == null) {
    26. /* Determine if it's an applet or not */
    27. /* If there is a security manager, ask the security manager
    28. what to do. */
    29. if (security != null) {
    30. /**
    31. * 返回线程组,在该线程组中实例化任何新的
    32. * 在调用此线程时创建的线程。
    33. * 默认返回当前线程的线程组。这应该被特定的安全管理器覆盖以返回适当的线程组。
    34. *
    35. * @return 新线程被实例化到的线程组
    36. * @since JDK1.1
    37. * @see java.lang.ThreadGroup
    38. */
    39. g = security.getThreadGroup();
    40. }
    41. /* If the security doesn't have a strong opinion of the matter
    42. use the parent thread group. */
    43. if (g == null) {
    44. g = parent.getThreadGroup();
    45. }
    46. }
    47. /*
    48. 确定当前运行的线程是否有权限修改这个线程组。 如果有一个安全管理器,
    49. 它的 checkAccess方法以这个线程组作为它的参数被调用。
    50. 这可能会导致抛出SecurityException。
    51. */
    52. g.checkAccess();
    53. /*
    54. * Do we have the required permissions?我们是否拥有权限
    55. */
    56. if (security != null) {
    57. if (isCCLOverridden(getClass())) {
    58. security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
    59. }
    60. }
    61. /*
    62. * 增加线程组中未启动线程的计数
    63. * 未启动的线程不会添加到线程组中,因此如果它们从未启动,则可以收集它们,但必须对它们进行计数,以便它们中具有未启动线程的守护线程组不会被破坏。
    64. */
    65. g.addUnstarted();
    66. //设置线程组
    67. this.group = g;
    68. //设置守护线程
    69. this.daemon = parent.isDaemon();
    70. this.priority = parent.getPriority();
    71. //设置类加载器
    72. if (security == null || isCCLOverridden(parent.getClass()))
    73. this.contextClassLoader = parent.getContextClassLoader();
    74. else
    75. this.contextClassLoader = parent.contextClassLoader;
    76. this.inheritedAccessControlContext =
    77. acc != null ? acc : AccessController.getContext();
    78. this.target = target;
    79. //设置优先级
    80. setPriority(priority);
    81. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    82. this.inheritableThreadLocals =
    83. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    84. /* Stash the specified stack size in case the VM cares 存放指定的堆栈大小,以备虚拟机关心*/
    85. this.stackSize = stackSize;
    86. /* Set thread ID 设置唯一线程id*/
    87. tid = nextThreadID();
    88. }

    启动线程

    线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义 是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

    1. public synchronized void start() {
    2. /**
    3. * This method is not invoked for the main method thread or "system"
    4. * group threads created/set up by the VM. Any new functionality added
    5. * to this method in the future may have to also be added to the VM.
    6. *
    7. * A zero status value corresponds to state "NEW".
    8. * 线程是否处于就绪状态
    9. */
    10. if (threadStatus != 0)
    11. throw new IllegalThreadStateException();
    12. /* Notify the group that this thread is about to be started
    13. * so that it can be added to the group's list of threads
    14. * and the group's unstarted count can be decremented.
    15. * 通知组该线程即将启动,以便可以将其添加到组的线程列表中,并且可以减少组的未启动计数。
    16. */
    17. group.add(this);
    18. boolean started = false;
    19. try {
    20. //start0 是一个本地的方法,start0()这个方法是在Thread的静态块中来注册的,其作用就是注册一些本地方法提供给Thread类来使用,比如start0()、isAlive()… ,具体方法在一个C的文件中Thread.c,其定义了各个操作系统平台要用的关于线程的公共数据和操作,具体定义了很多的方法都是各个操作系统能公共调用的,也就是线程在构建时候,会将这些方法注册上去
    21. start0();
    22. started = true;
    23. } finally {
    24. try {
    25. if (!started) {
    26. group.threadStartFailed(this);
    27. }
    28. } catch (Throwable ignore) {
    29. /* do nothing. If start0 threw a Throwable then
    30. it will be passed up the call stack */
    31. }
    32. }
    33. }

    中断线程

    中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。
    线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

    过期的suspend()、resume()和stop()

    大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

    终止线程

    除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程cancel(),通过中断操作和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

    等待/通知机制

    等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

方法名称 描述
notify() 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象锁
notifyAll() 通知所有等待在该对象上的线程
wait() 调用该方法的线程进入等待状态,只有等待其他线程获取到该对象锁,调用唤醒或中断方法才会返回,调用wait()方法,会释放锁
wait(long) 有时限的等待,超时后直接返回
wait(long,int) 对超时时间更细的控制

调用wait()、notify()以及notifyAll()时需要注意的细节:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
  5. 从wait()方法返回的前提是获得了调用对象的锁

    等待/通知的经典范式

    1. //等待方
    2. synchronized(对象) {
    3. while(条件不满足) {
    4. 对象.wait();
    5. }
    6. 对应的处理逻辑
    7. }
    1. //通知方
    2. synchronized(对象) {
    3. 改变条件
    4. 对象.notifyAll();
    5. }

    管道输入/输出流

    管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常

    Thread.join()的使用

    如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)

    ThreadLcal的使用

    ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值
    1. public class Profiler {
    2. // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
    3. private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
    4. protected Long initialValue() {
    5. return System.currentTimeMillis();
    6. }
    7. };
    8. public static final void begin() {
    9. TIME_THREADLOCAL.set(System.currentTimeMillis());
    10. }
    11. public static final long end() {
    12. return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    13. }
    14. }