Java 多线程(第一篇 - 一些原则和概念)
基本概念
线程和进程大家都了解的很多了,这里引用wiki百科的说明。
进程(process):是指计算机中已运行的程序。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机体系结构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。
https://zh.wikipedia.org/wiki/行程
线程(thread):是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
folder
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见的,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
https://zh.wikipedia.org/wiki/线程
并发(concurrent):实质是一个CPU在若干道程序(或线程)之间多路复用,并发是对有限的物理资源强行多用户共享使用以提高效率。
并行(parallel):两个或两个以上的事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源上(多核)同时执行。
引用一幅图来描述下:
线程安全:指在并发情况下,该代码经过多线程的使用,线程的调度顺序不影响结果。这时,我们只用关注系统内存,CPU是否够用即可。
线程不安全:意味线程的调度顺序影响最终结果。
时间片:时间片(timeslice)又称为量子(quantum)或处理器片(processor slice)是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
现代操作系统(windows,Mac OS X和linux等),允许同时运行多个进程,如在听音乐的同时用浏览器浏览网页。如果一台计算机只有一个CPU(非多核CPU),就不可能同时运行多个进程,只是这些进程“看起来像”同时运行,实则是轮番运行,由于时间片通常很短(linux上为5ms-800ms),用户不会感觉到。
时间片由操作系统的内核调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新给所有进程计算并分配新的时间片,如此反复。
通常状况下,一个系统中所有的进程被分配到的时间片长短并不是相等的,尽管初始时间片基本相等(在Linux系统中,初始时间片也不相等,而是各自父进程的一半),系统通过测量进程处于“睡眠”和“正在运行”状态的时间长短来计算每个进程的交互性,交互性和每个进程预设的静态优先级(Nice值)的叠加即是动态优先级,动态优先级按比例缩放就是要分配给那个进程时间片的长短。一般地,为了获得较快的响应速度,交互性强的进程(即趋向于IO消耗型)被分配到的时间片要长于交互性弱的(趋向于处理器消耗型)进程。
https://zh.wikipedia.org/wiki/时间片
线程状态
- 新建(NEW):创建了一个线程对象
- 可运行(RUNNABLE):创建线程后,其他线程(例如main线程调用了线程的start()方法,则该状态线程位于可运行的线程池中,等待被线程调度选中,获取CPU的使用权。
- 运行(RUNNING):可运行状态(RUNNABLE)的线程获得了CPU时间片(TimeSlice),执行线程代码。
- 阻塞(BLOCKED):阻塞状态是处于运行状态(RUNNABLE)状态的线程因为某原因放弃了CPU的使用权,即让出了TimeSlice,暂时停止运行,直到线程进入可运行状态(RUNNABLE),方才有机会再次获得TimeSlice转到运行状态(RUNNING),阻塞情况分为以下三种:
- 等待阻塞:运行状态(RUNNING)的线程执行了obj.wait()方法,JVM会把该线程放到等待队列(waitting queue)中;
- 同步阻塞:运行状态(RUNNING)的线程在获取对象的同步锁时,如果该同步锁被其他线程占用,则JVM会把该线程放进锁池(lock pool)中;
- 其他阻塞:运行状态(RUNNING)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()超时、join()等待线程终止或者超时、或I/O处理完毕时,线程重新进入可运行状态(RUNNABLE)。
- 死亡(DEAD):线程run()方法运行结束,或因异常退出了run()方法,则线程结束生命周期,死亡的线程不能再次复活。
ps:以下为csdn看到的解释,学习并作了一些修改性说明
在JAVA中,每个对象都有两个池,锁池和等待池。
wait(),notify()和notifyAll()都是Object类的方法。
- 锁池:假设
A线程
现在拥有某个对象的锁,而B线程
此时想要调用该对象的某个synchronized()方法(或synchronized代码块),B线程
在进入对象的sychronized方法之前必须获得对象的锁,但是该对象的锁正被A线程
拥有,所以B线程
就进入了对象的锁池。- 等待池:如果
A线程
调用了某个对象的wait()方法,A线程
就会释放已经拥有的对象的锁(因为wait()方法必须出现在synchronized中,这样在执行wait()方法时A线程
肯定是已经获得了该对象的锁的),同时A线程
进入到该对象的等待池中。如果另外一个线程调用了该对象的notifyAll()方法,那么处于该对象的等待池中的线程都会进入该对象的锁池中,准备争夺对象锁的拥有权。如果另外一个线程仅调用了对象的notify()方法,那么仅只有一个处于该对象的等待池中的线程会进入对象的锁池。
通过上图可以对线程的转换状态很清晰,只解释一下BLOCKED状态,线程在运行(RUNNING)状态可能会遇到阻塞(BLOCKED)状态:
- 调用join()和sleep()方法,线程会进入BLOCKED状态,sleep()时间结束或被打断,join()中断,IO完成都会回到RUNNABLE状态,等待JVM调度
- 调用wait()方法,线程会进入等待池(wait blocked pool),直到notify()或notifyAll(),线程被唤醒被放入锁池(lock blocked pool),释放同步锁会使线程回到RUNNABLE状态
- 对RUNNING的线程加同步锁(synchronized),会使线程进入锁池(lock blocked pool),释放同步锁会使线程回到RUNNABLE状态
- 处于RUNNABLE的线程处于等待被调度的状态,此时的调度顺序是不一定的。Thread类的yield()方法可以让一个正处于RUNNING状态的线程转到RUNNABLE状态
多线程的内存模型
内存模型
Java内存模型用来屏蔽各种硬件和操作系统的内存差异,达到跨平台的内存访问效果,JLS(Java language specification,java语言规范)定义了一个统一的内存管理模型JMM(Java Memory Model),JMM描述了Java多线程执行的内存访问方式。
首先,储存管理可以分为三个层面:硬件层面、操作系统层面、应用程序层面。
-
硬件层面:可以将存储体系分为外存、主存、高速缓存和寄存器,后三个统称为内存,对于CPU而言,寄存器就是CPU的工作内存,主存和高速缓存作为外存数据的缓冲。
硬件层面来说,CPU执行指令的速度非常快,远快于内存读取速度,为了缓解这种速度差,在CPU和内存之间,现代计算机都设计了很多级速度比内存快的高速缓存,储存了一些CPU频繁访问的变量,这样比直接去访问内存快很多。
-
操作系统层面:操作系统为了方便存储的管理,抽象出了地址空间的概念,将地址空间分段管理,分为代码段、数据段、共享段,数据段分为堆和栈等。
-
JMM是应用程序层为方便管理内存而抽象出来的内存模型,这种抽象模型依赖于操作系统地址空间的抽象,并最终落地到硬件层面,应用程序层就是为了屏蔽底层细节和差异,作为用户只要关心JMM是如何工作就行了,下图是JVM内存模型和硬件内存的关系。
- Java内存模型规定了Java所有变量都储存在主内存中,此处主内存仅为虚拟机内存的一部分,而虚拟机内存也仅是计算机物理内存的一部分(计算机为虚拟机进程分配的那一部分)
- Java内存模型分为主内存(main memory)和工作内存(working memory),主内存是所有线程共享的,工作内存不是共享的,每个线程自己有一个
- 线程的工作内存保存了被该线程使用到的变量的主内存副本,线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,不能直接读写主内存中的变量,不同线程也无法访问对方工作内存中的变量,线程之间变量值的传递必须要通过主内存来完成,他们三者的关系如下图:
Java内存间交互操作
JLS定义了线程对主存的操作:lock,unlock,read,load,use,assign,store,write,这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主存数据复制到当前工作内存,store-write用工作内存数据刷新主存数据。
-
lock:作用于主内存变量,把这个变量标示为某线程独占状态
-
unlock:作用于主内存变量,把这个变量释放独占状态
-
read:作用于主内存变量,把一个变量值从主内存传输到线程的工作内存,以便之后的load操作
-
load:作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中
-
use:作用于工作内存的变量,把工作内存的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时会执行这个操作
-
assign:作用于工作内存的变量,将一个执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
-
store:作用于工作内存的变量,把工作内存的一个变量传递给主内存,以便接下来的write操作
-
write:作用于主内存的变量,它把store操作从工作内存得到的变量的值放入放入主内存中
简单说你可以这么看
read是把变量从shared memory读入CPU local memory,或者说从内存读入CPU cache,write反之.
load是把变量从CPU local memory读入JVM stack,你可以认为它是把数据从CPU cache读入到“JVM寄存器”,store反之
多线程的三个特性
-
原子性(Atomicity)
原子性是指一个原子操作在CPU中操作中不可以暂停然后再调度,要么执行完成,要么不执行由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。
-
可见性(Visibility)
Java内存模型中的工作内存和主内存,解决了可见性问题。
volatile禁止了编译器对成员变量进行优化,读操作
时,JMM会把工作内存中对应的值设为无效,强迫从主内存中进行读取,写操作
时,JMM会把工作内存中的数据刷新到主内存中,这样在任何时刻不同线程看到的某一个成员都是最新值,保证了可见性。可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
-
有序性(Ordering)
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
指令重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或者处理器为了提高程序性能而做的优化。- 编译器优化的重排序(编译器优化)
- 指令集并行重排序(处理器优化)
- 内存系统的重排序(处理器优化)
Happen-before原则:
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用
这个前几个不太好理解,引用别人的博客happen-before原则详解