一. java多线程编程基本概念--------基本概念
java多线程可以说是java基础中相对较难的部分,尤其是对于小白,次一系列文章的将会对多线程编程及其原理进行介绍,希望对正在多线程中碰壁的小伙伴有所帮助。
(一)进程、线程和任务
进程可以认为是程序执行的一个实例,是程序向操作系统申请资源的基本单位,每个进程都拥有自己独立的地址空间。
线程是操作系统能够进行运算调度的最小单元,它被包含在进程之中,一个进程可以有多个线程。同一个进程中的多个线程共享这个进程的资源。
每个线程完成的计算被称为任务。
(二)串行、并发和并行
串行:按照顺序完成多个任务。
并发:在一段时间内以交替的方式完成多个任务。
并行:以齐头并进的方式完成多个任务。
并发往往是带着部分串行的并发,并发的极致就是串行。一个处理器就可以实现并发,但并行需要多个处理器在同一个时刻各自运行一个线程来实现。
多线程编程的实质就是将任务的处理方式由串行改为并发。
(三)多线程编程的优缺点
- 优点
提高系统的吞吐率;调高响应性;充分利用多核处理器;最小化对资源的使用(相对于多进程)
- 缺点
可能会造成线程安全问题、线程活性问题、引起上下文切换
(四)竟态
竟态是指一个计算结果的正确性和时间有关的现象。竟态往往伴随着读脏数据问题(线程读到的是一个过时的数据)、丢失更新(一个线程对数据所做的更新没有体现到后续其他线程对该数据的读取上)。
竟态通常是在多个线程在没有采取任何控制措施的情况下并发更新、读取同一个共享变量。竟态的两种模式为:read-modify-write(读-改-写)和check-then-act(检测而后行动)。
(五)线程安全问题
线程安全问题概括起来变现为3个方面:原子性、可见性和有序性。
1. 原子性
原子性是指访问某个共享变量的操作从执行线程以外的其他线程来看,该线程要么执行结束,要么尚未发生,即其他线程不会看到该操作的中间结果。java语言中,任何数据类型的读操作都具有原子性,除了long和double类型的基本数据类型和引用数据类型的写操作都具有原子性。
Java中有两种方式实现原子性:使用锁、CAS
2. 可见性
可见性用于描述一个行程对共享变量的更新对于另外一个线程而言是否可见的问题。可见性的保障是通过使更新共享变量的处理器执行冲刷处理器缓存,使读取共享变量的处理器执行刷新处理器缓存。
父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的,之后对于共享变量的更新对于子线称来说没有保障的。
3. 有序性
有序性用来描述一个线程的内存访问操作在另外一个线程看来是否有序的问题。
重排序可以分为一下三种:
1)编译器优化的重排序。编译器在保证貌似串行语义的情况下,可以重新安排语句的执行顺序。
2)指令级并行重排序:如果不存在数据依赖性,处理器可以语句对应机器指令的执行顺序。
3)内存系统重排序。由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去是乱序的。
(六)java内存模型和happens-before
Java内存模型使用happens-before关系来保证可见性和有序性。
1. java内存模型
java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
2. happens-before
happens-before有两层含义:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,第一个操作的执行顺序排在第二个操作之前。(这条是JMM对程序员的承诺)
2)两个操作之间存在happens-before关系,并不意味着java平台的具体实现必须要按照happens-before关系指定的顺序来执行,只要保证重排序之后的结果与按happens-before关系一致即可(这条JMM对编译器和处理器重排序的约束)。
貌似串行语义(as-if-serial)保证单线程内程序执行结果不改变,happens-before关系保证正确同步的多线程程序执行结果不改变。
具体规则:
- 程序顺序规则:同一个线程中的每个操作happens-before该线程中的任意后续操作。
- 内部锁规则:内部锁的释放happens-before后续每一个对该锁的申请
- volatile规则:对于一个volatile域的写happens-before后续每一个针对该变量的读操作
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C
- start()规则:如果线程执行操作B.start(),那么A线程中的B.start()操作happens-before B线程中的任意操作
- join()规则:如果A线程执行B.start()操作并成功返回,那么b线程中的任意操作happens-before A线程执行完B.start()返回后的任意操作。
- 程序中断规则:对线程iterrupted()的调用先行于被中断线程检测到中断事件的发生。
- 对象finalize规则:一个对象的初始化完成先行于它的finalize()方法的开始。
(七) 阻塞与非阻塞
阻塞和非阻塞用来形同多线程间的相互影响,一个线程占用了临界区资源,其他线程需要这个资源进行等待该资源的释放,会导致等待线程的挂起,这种情况就是阻塞,而非阻塞恰好相反,他强调没有一个线程可以阻塞其他线程,所有线程都会尝试着往前运行。