走进Java世界中的线程

系列阅读
1.深入理解Java虚拟机-GC&运行时数据区
2.深入理解Java虚拟机-类文件结构及加载
3.深入理解Java虚拟机-内存模型及多线程

start方法调用结束并不意味着相应的线程已经开始运行,运行时间有线程调度器决定

1. Java内存模型

主内存(Main
Memory)是各个线程共享的内存区域,所有的变量都存储在主内存中。线程间变量值的传递需要通过主内存来完成。

工作内存(Working
Memory)是每条线程都有属于自己的区域,工作内存保存了被该线程所使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)等都必须在工作内存中进行,而不能直接读写主内存中的变量。

勉强来说,主内存对应于物理硬件的内存,工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

bifa88唯一官网 1

处理器、高速缓存、主内存间的交互关系

主内存与工作内存之间的交互协议,即读写同步的操作是原子的,不可再分的,包括以下8中操作:lock/unlock/read/write作用于主内存变量,use/assign/store/load作用于工作内存。

运行结束的线程所占用的资源(如内存空间)会如同其他Java对象一样被JVM虚拟机垃圾回收

2. 线程同步

valatile同步
可以说是JVM中最轻量级的同步机制。
保证变量对所有线程的可见性,而普通变量不能保证这一点。
禁止指令重排序优化,保证变量赋值操作的顺序与程序代码的执行顺序一致。
优点:volatile变量读操作与普通变量几无差别,写操作时由于在本地代码中插入需要内存屏障质量来保证处理器不发生乱序执行,所以会慢一点。
volatile与锁之间选择的唯一依据是volatile能否满足使用场景的需求。

Java内存模型3大特性

  • 原子性
    可大致认为基本数据类型的访问读写是具备原子性的。synchronized块之间具备原子性。
  • 可见性
    指当一个线程改变了此值,新值对其他线程立即可见。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile/普通变量/synchronized/final。
  • 有序性
    如果在本线程内观察,所有的操作都是有序的。如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”的现象。valatile及synchronized可保证线程之间操作的有序性。synchronized规定了“一个变量在同一时刻只允许一条线程对其进行lock操作”。

线程的实现
线程的引入可以把一个进程的资源分配和执行调度分开,线程既可共享进程资源(内存地址、文件I/O等),也可独立调度(线程是CPU调度的基本单位)。
实现线程主要有3种方式:

  1. 使用内核线程实现
    轻量级进程(Light Weight Process,
    LWP)就是通常意义上的线程,每个LWP都由一个内核线程(Kernel-Level
    Thread,KTL)支持。
![](https://upload-images.jianshu.io/upload_images/3769423-7b1ccb740125167a.png)

轻量级进程与内核线程之间1:1的关系
  1. 使用用户线程实现
    广义上来说,一个线程只要不是内核线程,就可以认为是用户线程(User
    Thread,UT)。用户进程的建立、同步、销毁和调度完全在用户态中进行,不需要内核的帮助,所以,所有线程都需用户程序自己处理的话会异常困难。
![](https://upload-images.jianshu.io/upload_images/3769423-5c8508d0375ee3ae.png)

进程与用户线程之间1:N的关系
  1. 使用用户线程加轻量级进程混合实现
    这种混合实现下既存在用户线程也存在轻量级进程。用户线程完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。
![](https://upload-images.jianshu.io/upload_images/3769423-04e2a274640057d3.png)

用户线程与轻量级进程之间N:M的关系

线程调度
多线程系统的线程调度是指系统为线程分配处理器使用权的过程,主要调度方式为以下两种:
协同式调度:线程的执行时间由线程本身来控制;
抢占式调度:每个线程将有系统来分配执行时间。

线程的状态转换可参见Java并发编程学习笔记

为什么不直接调用run方法?

3. 线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的操作,单次调用都可以获得正确的结果,那这个对象就是线程安全的。

线程安全的实现方法

  1. 互斥同步
    同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。Java中可使用synchronized关键字和RetrantLock(重入锁)来实现同步,具体参见JAVA锁机制
  2. 非阻塞同步
    互斥同步主要问题是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也叫阻塞同步。
    非阻塞同步是基于冲突检测的乐观并发策略,先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果有争用,产生了冲突,那就再采取其他的补偿措施。这种实现大都不需要把线程挂起。为了让操作和冲突检测这两个步骤具备原子性,需要硬件指令集的发展和支持。
  3. 无同步方案
    同步只是保证共享数据争用时的正确性的手段。如果一个方法不涉及共享数据则无需任何同步措施去保证正确性。比如可重入代码和县城本地存储。

操作共享的数据类型

  1. 不可变
    不可变(Immutable)对象一定是线程安全的。如果共享数据是基本数据类型,只要定义用final修饰则是不可变;如果是一个对象,需要保证对象的行为不会对其状态产生任何影响,比如String/Number部分子类/Long/Double/BigInteger/DigDecimal等。

  2. 绝对线程安全
    一个类不管运行时环境如何,调用者都不需要任何额外的同步措施。

  3. 相对线程安全
    需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。Java中大部分线程安全类都属于这种,例如Vector/HashTable/Collections的synchronizedCollection()方法包装的集合等。

  4. 线程兼容
    对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。比如Vector/ArrayList/HashMap等。

  5. 线程对立
    无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。Java中很少出现。

注:主要内容摘录自书籍 深入理解Java虚拟机,周志明 著

如果在某处代码中直接调用某个线程的run方法,那么这个线程的run方法将在当前线程中运行,而不是在其自身线程中运行,违背了创建线程的初衷。

但是,确实是允许直接调用run方法的。

Thread类实现了Runnable接口

两种创建线程方式的比较

继承方式和接口方式,后者属于组合的技术,耦合性更低

后者的一个Runnable实例可以被多个线程实例共享

继承的方式创建线程,Java虚拟机会为其分配调用栈空间、内核线程等资源,成本更加昂贵

线程饥饿:

某些线程永远得不到运行机会,可能由于优先级使用不当导致。

守护线程和用户线程:

用户线程会阻止Java虚拟机的正常停止,一个Java虚拟机只有在其所有的用户线程都运行结束后才能正常停止;

守护线程则不会影响,一般用来执行一些重要性不是很高的任务,例如用于监视其它线程的运行情况。

通常情况下,一个线程是否是守护线程或者是用户线程,和其父线程保持一致。

工作线程(后台线程):

通常是其父类线程创建来用于专门执行某项特定任务的线程;

多线程编程的优势:

提高系统的吞吐率

提高响应性

充分利用多喝处理器资源

最小化对系统资源的使用

简化程序的结构

多线程编程的风险:

线程安全

线程活性

死锁

活锁:一个线程一直在尝试某个操作但就是没有进展

上下文切换

这是属于额外的资源消耗

可靠性

多线程编程的目标与挑战

串行、并发和并行

串行:按照顺序执行

并发:宏观上是同时进行,微观上轮流进行

并行:严格同时进行

多线程编程的实质就是将任务的处理方式由串行改为并发,即实现并发化,以发挥并发的优势。

竞态

一个计算结果的正确性与实践有关的现象,表现为一个问题,对于同样的输入,程序的输出有时候正确,有时候错误。

举例:多个线程对共享变量,进行i++操作

严格定义:

竞态(Race
Condition)是指计算结果的正确性依赖于相对时间顺序或者线程的交错。

注意:竞态不一定就导致计算结果的不正确,它只是不排除计算结果时而正确,时而错误的可能。

原子性

对于涉及到共享变量访问的操作,若该操作从执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性

即,其它线程不会“看到”该操作执行了部分的中间结果

Java中实现原子性的两种操作:

锁(Lock)

CAS(Compare-and-Swap)指令,俗称硬件锁

volatile关键字:

仅仅能保证变量写操作的原子性,不能保证读写操作的原子性

所以我们一般说,volatile只能保证可见性,不保证原子性。

可见性

多线程环境下,一个线程对于某个共享变量的更新,后续访问该变量的线程可能无法立刻读取到这个更新的结果,这就是不可见的情况。

可见性就是指一个线程对共享变量的更新的结果对于读取相应共享变量的线程而言是否可见的问题

可见性和原子性的联系和区别:

原子性描述的是一个线程对共享变量的更新,从另一个线程的角度来看,它要么完成,要么尚未发生。

可见性描述一个线程对共享变量的更新对于另一个线程而言是否可见

重排序:

重排序举例

new Instance()到底发生了什么

– 分配对象的内存空间

– 初始化对象instance

– 设置instance指向刚分配的内存地址

– 2和3可能发生重排序

重排序可能导致线程安全问题

重排序不是必然出现的

上下文切换:

一个线程被暂停,即被剥夺处理器的使用权,另外一个线程被选中开始或者继续运行的过程就叫做线程上下文切换

线程的活性故障:

死锁(Deadlock)

锁死(Lockout)

活锁(Livelock)

饥饿(Starvation)

资源争用和调度

公平调度策略:

按照申请的先后顺序进行授予资源的独占权

非公平调度策略:

没有按照先后顺序授予资源的独占权

非公平调度的解释:

在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该事件内,新来的线程(活跃线程)可以先被授予该资源的独占权。

如果新来的线程占用该资源的时间不长,那么它完全有可能在背唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。

非公平调度策略和公平调度策略的优缺点分析:

非公平调度策略:

优点:前者吞吐率较高,即单位时间内可以为更多的申请者调配资源;

缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象

公平调度策略:

优点:适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象

缺点:吞吐率较小

如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,我会持续更新后续章节学习笔记,可以点击原文链接更多精彩内容等着你

http://blog.sina.com.cn/s/blog\_16963d3590102xe8b.html

相关文章