首页 > 代码库 > Java 基础 - 多线程基础
Java 基础 - 多线程基础
并发
并发在单核和多核 CPU 上都存在, 对于单核 CPU,通过轮训时间片的方式实现并发.
线程
线程对象
利用Thread
对象, 有两种方式来创建并发程序:
- 直接创建并管理线程. 当程序要启动一个异步任务的时候, 直接创建一个线程.
- 将线程管理抽象出来, 把并发部分的任务交给
executor
.
线程的创建
有两种方式创建线程:
- 提供一个实现
Runnable
接口的对象. - 子类化
Thread
.
两种方法的优缺点?
Runnable
总体来说更好一点
- 使用
Runnable
接口的方式更加灵活, 因为可以继续子类化某个类 Runnable
接口的方式可以适配concurrent
包中的高级线程管理 API
线程的基本状态
线程有如下状态:
NEW
: 线程已经创建, 但还没有调用start()
开始执行.RUNNABLE
: 线程已经在 JVM 中开始运行, 但有可能在等待系统资源运行.BLOCKED
: 线程在等待一个 monitor lock 以进入一个synchronized
块. 也有可能是这个线程刚执行完wait()
, 其他线程又在获取wait()
对象的 monitor lock.-
WAITING
: 线程进入等待状态, 以下方法会使得线程进入wait()
状态:Object.wait()
join()
LockSupport.park
-
TIMED_WAITING
: 线程进入有时间限制的等待, 一下方法会使得线程进入此状态:Thread.Sleep
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos
LockSupport.parkUntil
-
TERMINATED
: 线程执行结束.
注意:
当线程 A 调用某个对象的 Synchronized
方法的时候, 线程就获得了这个对象的 intrinsic lock, 线程的状态是 RUNNABLE. 其他线程假如要获取这把锁, 就会进入 BLOCKED 状态.
当线程 A 调用wait
方法, 那线程A 会释放这个对象的锁(但是扔回持有其他对象的锁, 假如有的话), 然后线程转入 WAITING 状态. (若之前很多对象 pending 在这个 lock 上, 那么, 进入 wait 后会唤醒其他某个线程么?)
当其他某个线程 B 在同一个对象(也就是线程 A wait
释放的同一把锁)调用notify
或者 notifyAll
时, 线程A 状态由 WAITING 转化为 BLOCKED. 此时, 线程A 并不会自动获取到锁或者状态变成 RUNNABLE, 实际上, 线程 A 也要和其他被阻塞的线程一样竞争这把锁.
WAITING 和 BLOCKED 状态都会阻止线程运行, 但是区别却很大.
WAITING 状态必须被其他线程调用notify
从而显式的转化为 BLOCKED 状态. WAITING 状态从来不会直接转化为 RUNNABLE.
当一个 RUNNABLE 线程释放了锁(正常结束或者waiting
), 某个被阻塞的线程会自动被唤醒.
notify
和 notifyAll
区别?
notify
唤醒被同一把锁 wait
的第一个线程 notifyAll
唤醒同一把锁 wait
的所有线程, 但是优先级最高的先执行
Thread.sleep
Thread.sleep
导致当前线程暂时挂起一段时间, 其他线程可以有机会获取到 CPU 时间.
两个 API:
Thread.sleep(long ms)
Thread.sleep(long ms, long ns)
时间并不精确, 由于底层 OS 实现的限制.
Sleep 可以被打断, 当线程 A 在休眠, 而另外一个线程 B 调用 A.interrupt()时, 线程 A 就会抛出 InterruptedException
中断
中断
interrupt 会停止当前线程的正在进行的任务并且取做别的事情. 至于一个 thread 应该如何响应一个中断则是由程序员决定的.
响应中断
根据当前任务的长短, 做不同的处理:
-
当一个线程正在频繁的调用一个会抛出
InterruptedException
的方法时, 它可以通过try...catch
捕获, 并在catch
中做处理. 有很多方法会抛出InterruptedException
, 比如Thread.sleep
,sleep
的中断行为被设计成为:终止当前操作并抛出异常.for (int i = 0; i < ary.length; i++) { try { Thread.sleep(4000); } caatch (InterruptedException e) { return; } System.out.println(importantInfo[i]); }
-
当一个线程在执行一个长时间任务, 并且这个任务并没有抛出
InterruptedException
的时候. 那么就需要不停地去检测当前线程有没有被中断:for (int i = 0; i < inputs.length; i++) { heavyCrunch(inputs[i]); if (Thread.interrupted()) { // or return; throw new InterruptedException(); } }
中断的标志位
中断机制是由内部标识中断状态的一个标志控制的:
- 当调用
Thread.interrupt
时, 这个标志会被设置. - 当调用
Thread.interrupted
时, 标志会被清理. - 当调用
Thread.isInterrupted
查询中断状态时, 标志不变. - 任何方法假如因为
InterruptedException
而退出, 那么中断标志会被清理(但是有可能立即被设置).
Join
join
方法让一个线程可以等待另外一个线程执行结束后再往下执行. 和 sleep
一样, join
响应中断的方式也是退出并且抛出InterruptedException
.
线程同步
线程间通讯主要靠开放字段的访问或者字段引用的对象. 会带来两个问题:
- 线程干扰(thread interference)
- 内存一致性错误(memory consistency)
防止这两种错误的机制就是 线程同步. 然而, 线程同步会带来线程间竞争(thread contention). 饥饿和活锁都是 线程竞争 的表现.
线程干扰 - Thread Interference
指的是, 一个语句可能被虚拟机拆分成很多步执行, 然而当两个线程交叉执行时, 线程 A 的执行导致线程 B 的运行结果是不准确的. 比如, 线程 A 执行 c++
, 线程 B 执行 c--
, 开始两个线程读到 c 的值是0, 假如线程 B 在 A 之后执行完, 那么结果是 -1
而不是 0
.
内存一致性错误 - Memory Consistency Error
Memory Consistency Error 指的是线程对同一份数据却有不一致的值. 防止这种错误的关键是保证 happens-before 关系. 比如:
int count = 0;
线程 A 执行:
count++;
线程 B 打印 count
的值:
System.out.println(count);
那么线程 B 打印出的结果可能是 0
, 因为线程 A 的自增操作并没有和 B 的打印语句建立一个 happens-before 关系_.
happens-before 关系 保证的是, 某个语句所导致的内存写入动作会对另外的语句可见.
如何建立 happens-before 关系?
- 同一个线程, 前面的指令总比后面的指令先执行
- 释放 monitor lock(离开
synchronized
代码块或者方法), 总是发生在获取同一个 monitor lock(进入synchronized
代码块或者方法)之前. 由于 happens-before 关系的传播性, 释放锁的方法或者代码块, 总是比获取锁或者代码块之前执行. - 写入
volatetile
变量总是在读取前执行. - 调用
Thread.start
建立两种 happens-before 关系:Thread.start
前面的语句会在Thread.start
之前执行Thread.start
前面的语句会在新线程语句之前执行
- 线程 A 结束并导致线程 B
Thread.join
返回, 线程 A 中的所有语句会在线程 BThread.join
后面的语句之前执行
线程同步方法
Java 语言级别提供了两种方法:
- synchronized method
- synchronzied statements
注意:
- 构造函数不能用
synchronized
修饰, 否则会发生编译错误. - 对象的构造一定是在同一个线程中完成的.
-
不要提早的泄露字段的引用. 比如在构造函数添加下列的语句:
instances.add(this) //就会导致构造函数并未完成, 却将它通过 instances 暴露出去了.
Intrinsic Lock / Monitor Lock
同步的机制是建立在 intrinsic lock 又称 monitor lock 之上的. Intrinsic Lock 作用是
- 强制独占对象的状态(只要一个线程有一个 intrinsic lock, 其他线程是获取不到这个锁的)
- 建立一种 happens-before 关系
- 保证了状态更改的可见性.
synchronized method 中的锁
当线程调用一个 synchronized method 的时候, 线程会自动获得对象的 intrinsic lock. 并在下列情况释放:
- 方法正常返回
- 有未捕获异常发生
当线程调用一个 static synchronized method 的时候, 线程会获取与对象关联的 Class
对象的锁. 所以静态同步方法的锁和实例锁是不同的.
synchronzied statements
写法:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
注意: 在 synchronized method
或者 synchronzied statements
中要避免其他对象的同步代码(方法或代码块).
同步代码重入
- 一个线程不可以获得其他线程拥有的锁
- 一个线程可以获取自身已经拥有的锁
原子访问
意思是: 不可打断的操作
Java 中的原子操作:
- 读取或改变引用类型的引用
- 读取或改变基本类型(
long
和double
除外) - 读取或改变所有
volatile
类型的变量(包括引用,long
,double
)
原子操作不会被拆分, 所以不用担心线程干扰(Thread Interference)的问题, 但是却仍然要注意内存一致性(Memory Consistency)的问题.
使用volatile
可以避免内存一致性错误, 因为写入volatile
变量建立了一种 happen-before 的关系: 写入总比后续读先.
synchronized method
和 synchronized statements
会保证原子操作.
Liveness
Deadlock
死锁描述了这样一种情况: 两个或者多个线程进入永远的阻塞, 互相等待.
避免方法: 上锁的顺序相同.
如何排查? 通过 JStack 可以查看:
jstack <pid>
Java stack information for the threads listed above:
===================================================
"Thread-1":
at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)
- waiting to lock <0x0000000795706590> (a basic.DeadlockBower$Friend)
at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)
- locked <0x00000007957065d8> (a basic.DeadlockBower$Friend)
at basic.DeadlockBower$2.run(DeadlockBower.java:49)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)
- waiting to lock <0x00000007957065d8> (a basic.DeadlockBower$Friend)
at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)
- locked <0x0000000795706590> (a basic.DeadlockBower$Friend)
at basic.DeadlockBower$1.run(DeadlockBower.java:43)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
Starvation
饥饿描述了这样的情况: 线程无法长时间访问不到共享资源, 从而无法取得进展
Livelock
描述的是: 两个线程互相根据对方的行为做出响应, 导致各自没有实质性的进展
Guarded Blocks
线程之间经常协调他们的行为, 其中最常用的协调方法是 guarded block:
public void guardedJoy() {
// Simple loop guard. Wastes processor time. Don‘t do this!
while (!joy) { }
System.out.println("Joy has been achieved");
}
上面的代码通过不停 检测 joy
的状态来决定是否往下执行. 这样非常的耗费 CPU 时间.
更好的应该用 Object.wait()
来挂起当前线程.
public synchronized void guardedJoy() {
// This guard only loops once for each special event, which may not be
// the event we‘re waiting for.
while (!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
值得注意的是, 确保 wait()
在一个循环中, 因为你不能保证:
- 是
InterruptedExcpetion
还是正常唤醒导致wait()
结束 - 唤醒的线程是否将
joy
的值改变(尤其是在notifyAll
中)
消费者和生产者模式
解决了, 解耦了生产者和消费者, 并且更合理的运用了 CPU 时间.
不可变对象
不可变对象是那些构造后状态无法被改变的对象. 由于它的不可变性, 他不会有 Thread interference
和 Memory inconsistent
等问题.
不可变对象的特征:
- 不要设置
setter
方法. - 所有的成员都设置成
final
+private
- 不允许子类重写方法. 简单的方法是将类声明前 +
final
-
如果实例变量中有引用类型, 别让他们被更改:
- 不要提供更改他们的方法
-
不要共享他们的引用, 包括
- 不要在构造函数中直接引用参数
- 不要将实例引用变量返回, 如果不得不, 则, 返回拷贝!
Java 基础 - 多线程基础