线程
线程篇知识,主要包括并发编程方面,JUC并发包下的类的源码学习。
Thread
执行main方法后,就会开启一个jvm进程了,进程里,又有很多线程,main线程就是执行程序的第一个线程,然后我们又创建了一个子线程,来执行另外的任务。
1 | public class HelloWorld { |
多个线程的执行,是没有先后顺序的,他们会争夺和抢占的CPU的时间,谁先抢到,谁就先执行。
并发编程,无非就是在多线程的情况下去操作同一份数据,或者不同的线程之间需要通信。
Thread Group
ThreadGroup就是线程组,可以把一堆线程放入一个组里,作为一个整体统一管理和设置,这个一般不怎么用。
每一个线程都是会属于一个线程组的,如果在创建线程的时候没有设置,默认就是父线程的线程组。
比如main线程创建的子线程,子线程的线程组就是main ThreadGroup。
默认线程会加入父线程的ThreadGroup,也可以手动创建ThreadGroup,ThreadGroup也有父ThreadGroup,ThreadGroup可以包裹一大堆的线程,然后统一做一些操作,比如统一复制、停止、销毁,等等。
enumerate():复制线程组里的线程
activeCount():获取线程组里活跃的线程
getName()、getParent()、list(),等等
interrupt():打断所有的线程
destroy():一次性destroy所有的线程
1 | public class ThreadGroupDemo { |
优先级设置
优先级一般是在1~10之间,默认优先级是5,一般不设置,默认就是5,因为设置了,CPU也不一定按照这个来执行。
源码
看源码,一般就先扫一眼变量,或者从构造方法开始看起
1 | public Thread() { |
我们经常会看到日志打印里,线程名字都是Thread-0,Thread-1这样的,来源的代码就在这里了。
1 | private void init(ThreadGroup g, Runnable target, String name, |
(1)创建你的线程,就是你的父线程
(2)如果你没有指定ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup
(3)你的daemon状态默认是父线程的daemon状态
(4)你的优先级默认是父线程的优先级
(5)如果你没有指定线程的名称,那么默认就是Thread-0格式的名称
(6)你的线程id是全局递增的,从1开始
这是初始化的代码,接着看下start
1 | public synchronized void start() { |
sleep
Thread.sleep(500),可以让线程停顿一段时间,然后恢复运行,在很多场景都可能会用到,比如在死循环中,通过sleep方法来达到一个定时执行的效果。
yield
这也是一个Native的方法,很少看到有人会用到,它的意思就是让出CPU执行时间,让别的线程先去执行一下。
join
mian线程里创建线程start后,就会并发执行了,如果要实现等待的效果,可以调用线程的join方法,会阻塞等待子线程的逻辑执行完成,main线程继续往下走
1 | public static void main(String[] args) throws InterruptedException { |
interrupt
这个方法,叫打断,实际上他并不会中断线程的执行,只是给线程设置一个标志位,然后isInterrupted就能返回true了。一个最常见的案例:
1 | public class ThreadInterruptDemo { |
还有一种情况,就是一个正在sleep的线程,如果调用interrupt的话,也会中断睡眠,抛出一个java.lang.InterruptedException: sleep interrupted的异常。
线程的使用,基本上就是这些了,后面就是基于线程的并发编程。
并发编程的问题
在并发编程中,有三类问题,分别是可见性、原子性、有序性。
可见性
就是一个线程修改了变量值,另外一个线程读取到的还是原来的值的问题,在Java中一般用volatile关键字来解决,或者加锁。
原子性
比如i++的操作,他就是不保证原子性的,因为i++在底层是拆分成了多个指令,包含了读取,计算,写入,不同的线程在对同一个变量执行i++的时候,可能拿到的值是相同的,然后i++完成后都写入,导致最后的结果就不对了,比如i=1,2个线程i++开始读到的都是1,然后++完了,你以为最后应该是3了,其实都是2。
有人在会在网上说什么volatile是轻量级的锁,这是不对的,他并不能保证原子性,原子性只能通过加锁去解决,比如synchronize、lock,锁住变量,只能自己访问,操作串行化,保证多个操作之间的原子性。
有序性
有序性,就是指令重排序的问题,编译器和指令器,有时候会对代码进行优化,在前后逻辑不影响的情况下,他可能会优化代码的执行顺序,比如下面的代码。
1 | flag = false; |
那么重排序之后,有可能flag=true,就先执行了,可能就会导致线程2的代码, 执行异常。
基于happens-before保证有序性
指令重排序,不是乱排的,有个happens-before原则,只要符合happens-before原则的情况,就不能乱排。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
满足这8个原则的情况下,才能对指令进行重排序。