多线程学习笔记(一)
2018-09-06 本文已影响11人
42b551ef23a6
Java并发编程,对于平时只接触到CRUD的程序员来说,就像是一个看起来很美的冰山。明白进程和线程;知道Thread和Runnable等实现方式;也了解了线程池;Callable和Future的使用;但是在工作中,却少有机会去接触和实践。但是说实话,我也是从上一次跳槽面试开始,逐步去深入它的一些底层机制,试图一窥它的全貌,但目前看来,这条路还会很长,但也确实激发了我那么一点兴趣,希望能在以后的时间里,将自己的所学所想记录在这里,以供大家参考和自己复习。——2018.09.06
说明:本笔记参考gitchat作者追梦(加多)的内容精简整理,如有版权问题,请大家直接到gitchat进行订阅学习。
- 理解线程前务必先搞懂进程
- 线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程中的多个线程是共享进程的资源的。
-
操作系统分配资源是以进程为单位进行分配的,但是CPU资源比较特殊,它是分配到线程的,因为真正要占用CPU的是线程,所以才说线程是CPU分配的基本单位。
image.png
- 在内存中,线程占用的内存区域叫做“工作内存”,在工作内存中,有两个结构是必须要明白的,一个是程序计数器,另一个是栈。
- 程序计数器:用来保持当前线程代码执行到的具体位置。(因为CPU是通过时间片轮转的机制进行工作的,所以当前线程的时间片用完后,就需要让出CPU的执行权,等到CPU时间片再次轮询到自己时,继续接着上次执行到的地方执行,而程序计数器就是记录着线程的运行地址)粗暴点可以让为它启示录着当前程序执行到多少行了,只是它存储的实际是一个地址。
- 栈。栈用来存放当前线程独有的一些本地变量,也用来存放方法调用的栈桢。
- 线程的运行
- 线程调用start方法后并没有直接开始运行,而是在等待CPU的时间片。
- 什么是共享资源
- 所谓共享资源是说该资源被多个线程共享,多个线程都可以去访问或者修改的资源。另外本文当讲到的共享对象就是共享资源。
- 线程的通知与等待
- 注意:阻塞的线程有可能出现虚拟唤醒的情况,因此必须使用循环检查的方式。
synchronized (obj) {
while (条件不满足){
obj.wait();
}
}
- 等待线程执行终止的join方法
- 一旦调用了某个线程的join方法,则调用此方法的线程就会阻塞在join方法的位置,等待join的线程执行完成后,阻塞的方法才会继续执行。
- 注:由于 CountDownLatch 功能比 join 更丰富,所以项目实践中一般使用 CountDownLatch,后面会有CountDownLatch的说明
- 实例:待补充
- 让线程睡眠的sleep方法
- Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。当指定的睡眠时间到了该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,当获取到了 CPU 资源就可以继续运行了。如果在睡眠期间其它线程调用了该线程的 interrupt() 方法中断了该线程,该线程会在调用 sleep 的地方抛出 InterruptedException 异常返回。
- 实例:待补充
- 注: sleep 方法只是会让调用线程暂时让出指定时间的 CPU 执行权,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。
- 线程中断
- 注:中断一个线程仅仅是设置了该线程的中断标志,也就是设置了线程里面的一个变量的值,本身是不能终止当前线程运行的,一般程序里面是检查这个标志的状态来判断是否需要终止当前线程。
public class InterupptedDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("do some work");
}
});
thread.start();
Thread.sleep(1);
System.out.println("main thread interrupt child thread");
//中断子线程
thread.interrupt();
//等待子线程执行完毕
thread.join();
System.out.println("main is over");
}
}
- 理解线程的上下文
- 每个CPU上同时只能被一个线程占用,对于单个CPU来说,为了让程序看上去是并行执行的,CPU采用的是时间片轮转策略。当前线程使用完本次CPU分配的时间片后,就会让出CPU的执行权,自己变成就绪状态。并且为了在下次获得CPU时间片时知道之前的代码执行位置,就需要在让出CPU执行权时保存当前的执行现场。(执行现场存放在线程独有的程序计数器里面)
- 注:由于线程切换是有开销的,所以并不是开的线程越多越好,比如如果机器是4核心的,你开启了100个线程,那么同时执行的只有4个线程,这100个线程会来回切换线程上下文来共享这四个 CPU。
- 线程死锁
- 什么是线程死锁?
- 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
- 死锁产生必须具备以下四个必要条件
- 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其它进行请求获取该资源,则请求者只能等待,直至占有资源的线程用毕释放。
- 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其其它线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
- 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合{T0,T1,T2,···,Tn}中的 T0 正在等待一个 T1 占用的资源;T1 正在等待 T2 占用的资源,……Tn正在等待已被 T0 占用的资源。
- 如何避免线程死锁
要想避免死锁,需要破坏构造死锁必要条件的至少一个即可,但是学过操作系统童鞋应该都知道目前只有持有并等待和循环等待是可以被破坏的。- 造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁
- 注:编写并发程序,多个线程进行共享多个资源时候要注意采用资源有序分配法避免死锁的产生。
- 守护线程与用户线程
- Java 中线程分为两类,分别为 Daemon 线程(守护线程)和 User 线程(用户线程),在 JVM 启动时候会调用 main 函数,main 函数所在的线程是一个用户线程,这个是我们可以看到的线程,其实 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程(严格说属于 JVM 线程)。
- 如何创建守护线程呢?
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
public void run() {
}
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}