在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
1. 继承 Thread 类并重写 run()方法 extends 任务没有返回值
过程 1. 创建一个类继承Thread类2. 在main函数内创建一个该类的实例3. 然后调用该实例的 start()方法启动线程
2. 实现 Runnable 接口的 run()方法 implement 任务没有返回值
过程 1. 创建一个类实现 Runnable 接口2. 在main函数内创建一个该类的实例3. 然后使用该实例作为任务创建多个线程并启动这些线程
3. 使用 FutureTask 的方式
过程 1. 创建一个类实现 Callable 接口的 call()方法。2. 在main函数内首先创建了一个 FutrueTask 对象 (构造函数为该类的实例),3. 然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,4. 最后通过 futureTask.get 等待任务执行完毕并返回结果
三种方式对比
没有锁,醒后抢锁我们使用wait() 的目的是控制生产线程生产的速度
wait() 当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回其他线程调用了该共享对象的notify()或者notifyAll()方法其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回
wait (long timeout) 该方法相比wait方法多了一个超时参数(wait(0)和wait方法效果一样),如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms 时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回
notify() 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,也就是唤醒它的线程释放了共享变量上的监视器锁后,它必须和其他线程竞争获取了共享对象的监视器锁后才可以继续执行
notifyAll() 一个线程调用共享对象的notifyAll()方法后,会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程
join 用于多个线程加载资源,需要等待多个线程全部加载完毕后再汇总处理的情况。也可以说是 join把指定的线程加入当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程 B 中调用了线程 A 的 join 方法:那么直到 线程 A 执行完毕后才会继续执行线程 B
让CPU 不让锁 不让资源 (处于阻塞挂起状态,不参与线程调度)醒后抢 CPU使用sleep()目的是:可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会
当一个执行中的线程调用了 Thread 的静态 sleep 方法后,调用线程会暂时让出指定时间的 CPU 使用权,但是该线程所拥有的监视器资源,比如锁还是持有不让出的指定的睡眠时间到了后该函数会正常返回,然后处于就绪状态来参与CPU的调度,获取到CPU资源后就可以继续运行了如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则线程会在调用sleep方法的地方抛出InterruptedException异常而返回
让CPU 让资源 (不阻塞,参与线程调度) 醒后抢 CPU使用yield()的目的是让相同优先级的线程之间能适当的轮转执行
当一个线程调用了 Thread 的静态 yield 方法后,调用线程会让出剩余的 CPU 使用权,然后处于就绪状态来参与CPU的调度
线程中断是一种线程间的协作模式。 通过设置线程的中断标志并不难呢过直接终止该线程的执行,而是被中断的线程根据中断状态自行处理
(如果线程因为调用了wait系列函数、join方法或者seep方法而被阻塞挂起,这时候若调用该线程的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回)
从当前线程的上下文切换到了其他线程 在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换
那么让出 CPU 后下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时当前线程被其他线程中断时
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象
死锁产生条件
1. 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源
2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源
3. 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源
4. 环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程T0正在等待T1占用的资源,T1正在等待T2占用的资源……
如何避免线程死锁 想要避免死锁,只需要破坏掉至少一个构成死锁的必要条件即可。目前只有两个死锁条件可以被破坏:1. 请求并持有2. 环路等待 造成死锁的原因和申请资源的顺序有很大关系,所以进行资源的 有序分配 破坏了请求并持有条件和环路等待条件,因此避免了死锁
Java线程分为两大类:守护线程与用户线程
守护线程(daemmon) 在JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程 守护线程是否结束不影响JVM 的退出
用户线程(user) 在JVM 内部同时还启动了好多守护线程,比如垃圾回收线程 只要有一个用户线程还没结束,正常情况下JVM 就不会退出
注意:设置守护线程时,需要在线程启动start前将线程设置为守护线程
总结:如果你希望在主线程结束后 JVM 进程马上结束,那么在创建线程时可以将其设置为守护线程 如果你希望在主线程结束后子线程继续工作,等子线程结束后再让 JVM 进程结束,那么就将子线程设置为用户线程