认识线程
什么是线程
进程: 正常电脑中启动的某个程序应用,并且会获得计算机分配的资源(cpu,内存,硬件设备)
线程: 进程中为了完成某个功能,内部划分出的不同的资源分配单位。通常在多线程OS中,线程是计算机分配处理器的单位。当程序应用创建出的线程需要使用硬件设备,进行I/O读写文件的时候,就需要从用户级程切换为内核级线程,内核级线程是操作系统直接进行管理和调度的。
线程模型
线程模型: 用户级线程和内核级线程之间的对应关系。
多对一:
- 在多对一模型中,多个用户级线程映射到某一个内核线程上
- 线程管理由用户空间中的线程库处理,这非常有效
- 但是,如果进行了阻塞系统调用,那么即使其他用户线程能够继续,整个进程也会阻塞
- 由于单个内核线程只能在单个 CPU 上运行,因此多对一模型不允许在多个 CPU 之间拆分单个进程
从并发性角度来总结下,虽然多对一模型允许开发人员创建任意多的用户线程,但是由于内核只能一次调度一个线程,所以并未增加并发性。现在已经几乎没有操作系统来使用这个模型了,因为它无法利用多个处理核。
一对一:
- 一对一模型克服了多对一模型的问题,因为有多个内核级线程,所以可以在多个cpu之间切割任务
- 一对一模型创建一个单独的内核线程来处理每个用户线程,操作系统来管理和调度
- 但是,管理一对一模型的开销更大,涉及更多开销和减慢系统速度
- 此模型的大多数实现都限制了可以创建的线程数
多对多:
- 多对多模型将任意数量的用户线程复用到相同或更少数量的内核线程上,结合了一对一和多对一模型的最佳特性
- 用户对创建的线程数没有限制
- 阻止内核系统调用不会阻止整个进程
- 进程可以分布在多个处理器上
- 可以为各个进程分配可变数量的内核线程,具体取决于存在的 CPU 数量和其他因素
java线程和操作系统的线程的区别
java线程在不同的操作系统中有不同的实现方式,Linux下是基于pthread库实现的轻量级进程,Windows下是原生的系统Win32
API提供系统调用从而实现多线程,所以本质上java线程就是基于操作系统的线程
java线程状态
查看java.lang.Thread类的源码,可以发现java中的线程状态有六种:
// 尚未启动的线程的线程状态。
NEW,
/**
* 可运行线程的线程状态。可运行的线程状态在Java虚拟机中执行,但可能正在等待来自操作系统的其他资源,例如处理器。
*/
RUNNABLE,
/**
* 该线程被阻止等待监视器锁定。处于阻塞状态的线程正在等待监视器锁定
* 等待进入同步块/方法,或调用Object.wait()方法之后被重新唤醒等待进入同步块/方法
* 调用wait方法之后的线程状态变化历程为: WAITING--> RUNNABLE ----> BLOCKED
*/
BLOCKED,
/**
* 线程变为WAITING状态可能是因为调用了
* Object.wait()方法 (没有超时参数)
* Thread.join() (没有超时参数)
* LockSupport.park()
*/
WAITING,
/**
*
* 定时等待,在WAITING状态的基础指定了时长,可能是调用下面的方法
* Thread.sleep(long millis)
* Object.wait(long timeout)
* Thread.join(long millis)
* LockSupport.parkNanos(Object blocker, long nanos)
* LockSupport.parkUntil(Object blocker, long deadline)
*/
TIMED_WAITING,
/**
* 完成执行
*/
TERMINATED;
Thread类常用方法
start/run方法:
run方法是实际线程运行代码逻辑的方法,是从实现Runnable接口中得来的,但是如果直接调用run方法并不会启动一个线程,而是一个普通的调用对象实例的方法;
start方法才是真正启动一个线程的方法,start方法是用native关键字修饰的方法,表明是一个本地方法。我理解的本地方法就是用其他语言写好的一个方法,加上native关键字之后就会区分出本地方法,在JVM内存模型中有一个特定的区域就是本地方法栈,是线程私有的区域。在这个native方法中,应该会去创建一个线程并去调用写好的run方法。
sleep / yield
调用sleep方法之后线程状态会变为TIMED_WAITING状态,线程会放弃CPU资源,但是不会放弃锁,
调用yield无参方法之后线程会进入WAITING状态,但是如果调用的是yield有参方法,则会进入TIMED_WAITING状态;
yield虽然也会放弃cpu资源,但是会倾向于让更高优先级的线程先执行。
interrupt方法
使用interrupt方法会将线程的是否中断标志设为true,但是我看这个方法的源码里并没有这个设置过程,在Thread也没有找到这个中断标志位,而是去调用了一个native方法去完成这个工作。
如果当前线程已经处于BLOCKED,WAITING,TIMED_WAITING状态,再去调用这个线程的interrupt方法,会抛出InterruptedException异常,可以在捕获这个异常的catch块中做结束动作,同样的如果线程已经调用了inerrupt方法,再去进入BLOCKED,WAITING,TIMED_WAITING状态也会抛出同样的异常。
setDaemon
在Thread类中有一个 boolean daemon 属性,是否设置为守护线程,默认为false,如果设置true,当程序中没有其他线程运行的时候守护线程会自动结束。java中GC回收就是守护线程。
线程如何运行
线程的运行,包括java代码的运行都和JVM息息相关。
java运行时区域(JVM)
Java堆(线程共享)
所有的对象实例都在这里分配内存
方法区(线程共享)
它用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
本地方法栈(线程私有)
就是java代码中用native关键字修饰的方法
java虚拟栈(线程私有)
主要存储的是局部变量表,操作数栈,动态链接,返回地址等等。 虚拟栈就是一个一个帧栈组成的,而每一个帧栈对应的就是java中的每一个方法,没调用一个方法,就会创建一个帧栈。
程序计数器(线程私有)
如果执行的是普通java代码,存放的就是下一条需要执行的jvm字节码指令的地址;如果执行的是native方法,则为空。
线程安全
线程不安全的问题就是多个线程同时访问和操作同一个资源导致出现意料之外的问题。
java内存模型(JMM)
JMM规定了所有变量都存储在主内存中,每个线程会有自己的工作内存,其中工作内存中存储的变量都是主内存中的变量的副本,线程对变量的访问或者更新操作都必须在本工作内存中进行,然后再刷新到主内存中。
JMM的三大性质:原子性,可见性,有序性。
java实现原子性
java提供了锁和循环CAS的方式保证了原子性,这里的锁包括了synchronized关键字,也有 java.util.concurrent.locks.Lock 接口的锁;
循环CAS则是使用了CAS算法的代码类,比如原子类AtomicInteger,AtomicLong等等这些原子工具类。
CAS算法就是在线程想要更新变量的时候有三个操作:
- 先到主存中获取到要更新的这个变量的最新值
- 和之前获取主存中的这个变量进行比较
- 如果之前获取的值和最新获取的值一样,那就更新;否则就自旋重试或者阻塞
简短点概括就是: 获取-----比较------更新
java实现可见性
实现的方式有volatile关键字,synchronized关键字,final关键字修饰变量也可以实现。
final关键字也能实现的原因是:
只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(lock、volatile)就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。
java实现有序性
java提供了synchronized和volatile关键字来保证操作之间的有序性。
Happens-before 原则
happens-before原则定义如下:
(1): 如果第一个操作happens-before于第二个操作,那么第一个操作的结果对于第二个操作就是可见的,并且第一个操作的顺序排在第二个操作的前面
(2): 在不影响happens-before结果的前提下,允许对happens-before的操作进行 重排序
as-if-serial只能作用单线程,happens-before则是提供跨线程的内存可见性保证。
as-if-serial: cpu和编译器在对指令进行重排序的时候,不管如何重排,单线程环境下都不能改变执行结果。