为什么需要多线程
- CPU:对于CPU的计算速度来说,其它的操作都太慢了
- CPU经常需要等待网络,内存,磁盘IO等操
- 1GHZ -> 1G(10^9) 时钟周期 / 秒 -> 1纳秒
- 现代CPU都是多核了
- Java的执行模型是同步/阻塞(block)的
- 默认情况下只有一个线程
- 处理问题非常自然
- 但是有严重的性能问题
开启一个新线程:Thread
- Java中只有Thread这么一种东西代表线程
- start方法才能并发执行,run方法是等这步执行完后才进行下一步
- 每多开一个线程,就多一个执行流
- 方法栈(局部变量)是线程私有的
- 静态变量/类变量是被所有线程共享的
- 共享变量可以收集所有线程的结果
- 变量的共享,是多线程几乎所有坑的来源
多线程的难点
- 线程困难的本质:同一份代码,你要想象不同的人在疯狂地以乱序执行它
- 非原子操作,都是线程不安全的
- 比如i=0;一千个线程执行i++操作,最后得到的i小于1000
- 因为操作系统在不停切换线程,这个线程事情没做完,切到另一个线程做了些事情再切回来
多线程适用的场景&带来的性能提升
1. 适合的场景
IO密集型应用
对于CPU密集型应用稍有折扣
- 因为多线程的本意是CPU运算速度对于其他操作太快了
- 在等待其他操作时让CPU不要闲着
- 从而提示性能
-
3. 性能提升的上限在哪?
单核CPU: 100%
- 多核CPU: N*100%(N核跑满)
线程的昂贵性
- 线程无法无穷无尽地提升性能
- 线程的昂贵性在于:
- CPU切换上下文很慢
- 线程需要占用内存等系统资源
- 如果你的应用用户数量较低:
- new Thread().start()
- 如果应用负载很高:
- 使用线程池:JUC包
线程安全
- 享用了线程的便利需要付出代价
- 原子性
- 共享变量
- 默认的实现几乎都不是线程安全的
线程不安全的表现
1. 数据错误
public class Main {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
// 两个进程以不同顺序获取lock
// Thread2等不到lock1,Thread1等不到lock2
// start方法才能并发执行,run方法是等这步执行完后才进行下一步
new Thread1().start();
new Thread2().start();
}
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (lock1) {
System.out.println("Thread1 get lock1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1 get lock2");
}
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (lock2) {
System.out.println("Thread2 get lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2 get lock1");
}
}
}
}
}
案例:著名的HashMap的死循环问题
- coolshell:https://coolshell.cn/articles/9606.html
3. 预防死锁的原则:
- coolshell:https://coolshell.cn/articles/9606.html
-
4. 死锁问题的排查
-
5. 多线程的经典问题:哲学家用餐
查看Java进程
- Linux命令查看进程: ps | grep java
- Java自带命令查看所有Java进程: jps
- jstack +
查看进程中的所有堆栈信息 - JVM所有对象都在堆(heap)上分配
- 每个线程独享一个方法栈
- 每个方法调用别的方法,就在栈上加一层
- 每个线程最底下就是Thread.run()方法
实现线程安全的基本手段
1. 不可变类:Integer、String…
- 两个以上线程修改一个对象就可能出问题
-
2. synchronized 同步块
同步块同步了什么东⻄?
- synchronized(Object) 把这个Object当成锁
static synchronized ⽅法
把Class对象当成锁private synchronized static void func() {}
- 实例的 synchronnized⽅法 把该实例当成锁
Collections.synchronizedCollection/List/Map/…
3. JUC包
AtomicInteger/Boolean/…
- ConcurrentHashMap
- 任何使⽤HashMap有线程安全问题的地⽅
- 都⽆脑地使⽤ConcurrentHashMap替换即可。
- ReentrantLock
- 可重入锁,与synchronnized相似,但是更强大
- …
Object类中的线程方法
1. 线程的历史
经典的生产/消费者模型
三种实现方法
- wait/notify/notifyAll
- Lock/Condition
- BlockingQueue