多线程介绍
Why 并发编程?
CPU通过时间片分配算法来执行指令。有关Java并发编程,以下是减少上下文切换的工具:
使用性能分析工具Lmbench3测量上下文切换时长
使用vmstat观察上下文切换次数
通过无锁并发编程、CAS算法、协程等方式进行优化。
Java线程简述
何谓线程
现代操作系统在运行一个程序时,会为其创建一个进程,在一个进程里可以创建多个线程。线程是现代操作系统调度的最小单元。
现代操作系统基本采用时分的形式调度运行的线程,线程会分配到若干时间片,时尽则调度,时间片多少也就决定了线程使用处理器资源的多少。而线程优先级就是决定线程需要的处理器资源的线程属性,在 Java 线程中,通过一个整型成员变量 priority 来控制(范围从1 ~ 10),在线程构建的时候可以通过 setPriority(int) 方法来修改。
线程优先级不能作为程序正确性的依赖,因为操作系统可以不理会 Java 线程对于优先级的设定 。
Thread类中的start方法可以启动新线程,同时调用run方法。
Runnable接口实现了run方法,供方法的调用。

线程的构造和启动
Daemon 线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作,可设置。
线程构造
运行线程前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是 Daemon 线程等信息 。
建议在线程启动前,为这个线程设置线程名称,便于调试。
线程启动
利用
Thread子类的实例(继承)创建
Thread的子类,并利用它来启动。如new aThread().start()重写
run方法,调用start方法创建线程。如果直接调用run方法,会在主线程运行。利用
Runnable接口调用
Runnable接口的实现run方法的类。这种方式相较于继承,便于资源共享。如new Thread(new aRunner()).start();
-- 上面两种方法都是无返回值的,若需要返回值,则:
实现
Callable接口,重写call方法,通过FutureTask获取返回值(参:机制)
此外,通过线程池(Executors)也能创建线程。
线程的暂停
sleep方法是Thread类的静态方法,注意由Thread调用。
线程的互斥与协作
synchronized方法
synchronized方法同步方法,每次只能由一个线程运行,其获取了一个防止其他线程进入的锁。
非synchronized的方法不受锁的影响。
每个实例拥有一个独立的锁。
synchronized代码块
synchronized(expr){...}可以使方法中的某一部分由一个线程进行而非整个方法。
synchronized静态方法
synchronized静态方法也是每次只能由一个线程运行,但其锁机制比较特别:使用类对象的锁执行互斥处理。

线程的协作
下面三个方法均是Object类的方法:
(this.)wait:当前线程暂停运行并进入调用者obj的等待队列中,释放锁。notify:唤醒等待队列中的其中一个(未定义)线程。notifyAll:唤醒等待队列中的所有线程。推荐使用。
这里有一些建议:
使用 wait、notify和 notifyAll时先对调用对象加锁 。
notify 或 notifyAll 方法调用后,等待线程依旧不会从 wait 返回,即,从 wait 方法返回的前提是获得了调用对象的锁 。需要调用notify() 或 notifyAll() 的线程释放锁之后,等待线程才有机会从 wait返回。
等待 - 通知模式
等待方:
通知方
等待-超时模式
在等待-通知范式下略作修改,得到“超时则返回默认值”的模式。
join方法
如果一个线程 A 执行了 thread.join() 语句 , 其含义是 :当前线程 A 等待 thread 线程终止后才从thread .join()返回。
线程的状态迁移

习题




Java内存模型 - 多线程篇
并发编程两大关键问题:线程间如何通信?线程间如何同步?
JMM通过控制主内存与每个线程的本地内存之间的交互,来进行可见性保证。
内存屏障
Java编译器会在生成指令序列的特定位置设置屏障,以禁止特定类型的处理器重排。

根据顺序一致性模型,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
Happens-before机制
happens-before是JMM中最核心的概念。
JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且 B happens-before C,那么A happens-before C。

start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

可见性
可见性:线程A对字段x写入值,线程B可读到该值,则“线程A向x的写值对线程B是可见的”。
多线程程序中,如果没有使用synchronized或volatile正确同步,线程A向字段的写值对线程B不是立即可见的。

内存模型综述
Java 程序的内存可见性保证可以分为下列 3 类 。
单线程程序不会出现内存可见性问题 。
正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同) 。
未同步 / 未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值( 0/null/false) 。
内存语义
volatile的内存语义
具有“同步处理”和“对 long 和 double 进行原子操作”两大功能。
简而言之,volatile变量自身具有下列特性。
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
某个线程对volatile字段的写操作结果对其他线程立即可见。volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义如下:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
可以通过设计内存屏障的方式,实现内存语义,举例:

volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性 。
注意:volatile不会进行线程的互斥处理。
JDK 5后,严格限制 volatile 变量与普通变量的重排序,使 volatile 的写-读和锁的释放 - 获取具有相同的内存语义。
锁的内存语义
我们来看一个例:

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
配合上述内存语义,结合AQS(非阻塞数据结构和原子变量类),助力concurrent包实现。
final的内存语义
final字段初始化的值对所有线程可见。
对于final域,编译器和处理器要遵守两个重排序规则。
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域:JMM禁止编译器把final域的写重排序到构造函数之外。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。
确保构造函数处理结束时,字段值被正确初始化。
读final域:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。
如果final为引用类型呢?对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

JDK5增强 final 的内存语义:保证 final 引用不会从构造函数内逸出的情况下,final 具有初始化安全性 。
synchronized
相当于{ 时获取锁,}时释放锁。
Java内存模型确保某个线程在进行unlock M前的所有写入操作对执行lock M的进程可见。
关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性 。
多线程程序的评价标准
安全性:不损坏对象
如果一个类被多线程同时调用,会导致对象的状态与设计者不一致,则为线程不安全的。
如果非线程安全者经过适当的互斥处理,则称为线程兼容。
生存性:必要的处理能够被执行
死锁就是一个典型的反例。
可复用性:类可重复调用、利用。
性能:能快速、大批量进行处理。
影响性能的因素有:
吞吐量:单位时间内能完成的处理数量。
响应性:发出请求到收到响应的间隔。
容量:可同时进行的处理数量。
安全性和生存性是必须遵守的标准,在此基础上提高可复用性和性能。

参考: 《图解Java多线程设计模式》
最后更新于
这有帮助吗?
