什么是线程
在了解什么是「线程」之前,我们先了解一下什么是「进程」
进程是线程的母亲,进程是系统进行资源分配和调度的基本单位
,是操作系统结构的基础。进程是线程的容器,进程中可以容纳多个线程。
简单来讲就是
:一间房子相当于一个容器,也就是进程的概念。这间房子住着你和你的父母,三个人就是分别的线程。你上学,妈妈干家务,爸爸上班,做着不同的事情,维持的进程的运行。
「线程」就是轻量级的进程,是程序执行的最小单位,使用多线程而不是多进程来进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程
。
线程所有状态的State如下:
NEW
状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()
方法调用时,才表示线程刚开始执行。当线程执行时,处于RNNNABLE
状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized
同步块,就会进入BLOCKED
阻塞状态,这是线程就会暂停执行,直到获得请求的锁。WAITTING
和TIMED_WAITING
都表示等待状态,它们的区别是WAITING
会进入一个「无时间限制」的等待,TIMED_WAITING
会进行一个「有时间限制」的等待。一般来说,WAITING
的线程正是在等待一些特殊的时间。比如,通过wait()
方法等待的线程在等待notify()
方法,而通过join()
方法等待的线程则会等待目标线程的终止。一旦等待了期望的时间,线程就会再次执行,进入RUNNABLE
状态。当线程执行完毕后,则进入TERMINATED
状态,表示结束。
线程的基本操作
新建线程
使用 new
关键字创建一个线程对象,并调用 start()
方法即可
Thread thread = new Thread(); thread.start();
start()
方法执行后,实际上会调用线程中的run()
方法,start()
方法就会新建一个线程并让这个线程执行run()
方法。
Thread thread = new Thread(); thread.run();
如果直接调用线程中的run()
方法,那么只是作为一个普通方法的调用。
不要用 run() 方法来开启新线程,它只会在当前线程中 串行 执行 run() 方法中的代码。
按照上面的方法新建线程,实际上是没有执行任何操作的,如果我们要实现自定义的方法,那么就要重写run()
方法,把自定义的任务传进去。这个时候就可以使用Thread
的另外一个构造方法了。
Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("这里定义自定义的任务"); } }); thread.start();
Runnable接口是一个单方法接口,它只有一个 run()方法
@FunctionalInterface public interface Runnable { public abstract void run(); }
终止线程
一般来说,线程执行完毕就会结束,不需要手动关闭。但是如果一些服务端的后台线程常驻系统后台,它们通常不会正常终结。它们的执行体本身就是一个大大的无穷循环,用于提供某些服务。
我们可以使用 stop()
将一个线程立刻终止
通过图片我们可以看到stop()
方法已经被注解@Deprecated
标记,说明是已经废弃的方法。这是因为stop()
方法过于暴力,它会强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。所以除非你很清除自己在做什么,否则不要随便使用stop()
方法来停止一个线程。
我们可以定义一个标识,来控制线程何时退出
线程中断
线程中断是一个重要的线程协作机制,线程中断并不会向stop()
方法那样使线程马上停止,而是给线程发送一个通知,告知目标线程,你可以退出了。Thread
中提供了三个与线程中断有关的方法。
public void interrupt() {} //中断线程 public boolean isInterrupted() {} //判断是否被中断 public static boolean interrupted() {} //判断是否被中断,并清除当前中断状态
这种方法看起来与前面设置 stopSelf()
方法相似,但是中断的功能更为强劲,如果循环体中,出现了类似wait()
方法或者sleep()
方法这样的操作,则只能通过中断来识别了。
等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程:等待wait()
方法和通知notify()
方法。这两个方法并不是 Thread 类中的,而是在 Object 类中。这就意味着任何对象都可以调用这两个方法。
当 A 线程调用了 wait()
方法后,A 线程就会停止执行转为等待状态。一直等到其他线程调用了 notify()
方法为止。
如果一个线程调用了wait()
方法后,就会进入 object 对象的等待队列。这个等待队列可能会有多个线程。因为系统运行了多个线程同时等待某一个对象。当有其他线程调用notify()
方法后,会随机的从这个等待队列中选择一个线程进行唤醒,这个选择是不公平的,并不是先等待的线程就会优先选择。
注意
:wait() 方法并不能随便调用,它必须包含在对应的synchronized语句中,因为无论是 wait() 方法还是 notify() 方法都需要获取目标对象的一个监视器,在 wait() 方法执行后,会释放这个监视器。
*wait()
方法和sleep()
的区别就是:*wait() 方法会释放目标对象的锁,而 sleep() 则不会
挂起(suspend)和继续执行(resume)线程
挂起(suspend) 和 继续执行(resume)这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()
方法操作后才能继续执行。当时这两个也是被标注为废弃的方法。
这是因为suspend()
方法在导致线程暂停的同时,并不会释放任何资源,此时,其他任何线程想要访问被它占用的锁时,都会被前两,导致无法正常继续执行。直到对应的线程上进行了resume()
方法操作,被挂起的线程才能继续执行,从而其他所有阻塞在相关锁上的线程也可以继续执行。
重要的是这两个方法不能保证有序性。也就是说resume()
方法可能会发生在suspend()
方法的前面,那么结果就是,挂起线程的锁永远不会被释放,进入死锁状态,将会导致整个系统工作不正常!
等待线程结束(join) 和 谦让(yeild)
有些时候,一个线程的输入可能需要依赖其他线程的输出,这个时候,这个线程就需要等待其他线程执行完成才能继续执行。那么就需要通过join()
方法来协助。JDK中有两个join()
方法:
public final void join() throws InterruptedException public final synchronized void join(long millis) throws InterruptedException
第一个join()
方法表示无限等待,它会一直阻塞线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程就会停止等待,继续往下执行。
在主函数中,如果不使用join()
方法等待 AThread 线程执行结束那么打印 i 的结果是0,使用了 join()
方法后,打印 i 的结果是10000。
而yield()
方法则是一个静态方法,它会使当前线程让出CPU。但是注意的是,让出CPU并不表示当前线程不执行了,当前线程让出CPU后,还会进行CPU资源的争夺,但是是否能够再次分配到就不一定了。
如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用yield()
方法,给予其他重要线程更多工作机会。
线程组的使用
守护线程(Daemon)
守护线程是一种特殊的线程,它是系统的守护者,在后台默默地完成一些系统性的服务,如「垃圾回收线程」,「JIT线程」就可以理解为守护线程。
与之相对应的就是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部完成,那么这个程序实际上就无事可做了,守护线程要守护的对象已经不存在了,整个应用程序就应该结束。因此,当一个 Java 应用内只有守护线程时,Java 虚拟机就会自然退出。
守护线程的使用:
线程优先级
Java 中线程可以有自己的优先级,优先级高的线程在竞争资源时会有更多的优势,更可能抢占资源,当然这也是一个概率问题,有些时候设置高优先级的线程并不管用。
在 Java 中使用 1-10 来表示线程优先级,一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;
上图中,将 A 线程的优先级设置为 10,B 线程的优先级设置为1,可以看到最后先打印的是 “A 线程执行完毕”,但是因为这种方法有概率性的,因为并不会总是打印 “A 线程执行完毕”。