摄影分享~~
文章目录
- 线程的状态
- 多线程带来的风险
- 线程安全
- 线程安全的原因
- 解决线程不安全问题(加锁)
- synchronized关键字-监视器锁monitor lock
- synchronized的特性
- java中的死锁问题
- 死锁
- 死锁的三个典型情况
- 死锁的四个必要条件
- 如何避免死锁?
- Java 标准库中的线程安全类
线程的状态
状态是针对当前的线程调度的情况进行描述的。
线程是调度的基本单位,状态是线程的属性。
- NEW:创建了Thread对象,但是还没调用start(内核中还没有创建PCB)
- TERMINATED:表示内核中的pcb已经执行完毕了,但是Thread对象还在。
- RUNNABLE:可运行的(包括正在CPU上执行的和在就绪队列中随时可以去CPU上执行的)
- WAITING
- TIMED_WAITING
- BLOCKED
4~6三个状态都是阻塞状态。(都是表示线程PCB正在阻塞队列中)只不过是不同原因的阻塞。
TERMINATED状态中,内核中线程的PCB被释放了,此时代码中的t对象也就没用了。Java中对象的生命周期自有其规则,这个生命周期和系统内核中的线程并非完全一致,**内核的线程释放的时候,无法保证Java代码中的t对象也立即释放。**此时t对象标识为:无效。虽然t对象无效了,但是t对象依旧可以完成调用函数等操作。
一个线程只能start一次。
public class ThreadDemo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 1000_0000; j++) {
int a = 10;
a += 10;
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
//未start之前是new状态
System.out.println("start之前:"+t.getState());
t.start();
//t执行中的状态runable
for (int i = 0; i < 1000; i++) {
System.out.println("t 执行中的状态: " + t.getState());
}
t.join();
// 线程执行完毕之后, 就是 TERMINATED 状态
System.out.println("t 结束之后: " + t.getState());
}
}
多线程的意义:
多线程可以更充分利用多核心的CPU资源,从而加快程序的运行效率。
多线程带来的风险
线程安全
线程安全的问题的根本原因就是抢占式执行,带来的随机性。
我们来看一个例子:
class Counter {
public int count = 0;
public void add() {
count++;
}
}
public class ThreadDemo12 {
public static void main(String[] args) {
Counter counter = new Counter();
// 搞两个线程, 两个线程分别针对 counter 来 调用 5w 次的 add 方法
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
// 启动线程
t1.start();
t2.start();
// 等待两个线程结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的 count 值
System.out.println("count = " + counter.count);
}
}
以上代码我们预期结果是100000次,但是运行结果如下:
为什么出现这个bug呢?
count++;本质上在操作系统中分成三 步:
- 先把内存中的值,读取到CPU的寄存器上(load)
- 把CPU寄存器中的值进行+1操作。(add)
- 把读到的结果写到内存中。(save)
当两个线程并发执行count++时,就相当于load add save同时执行。此时就会产生结果上的差异。
可能执行的方式:
箭头是时间轴,靠上就是先执行,靠下就是后执行。
由于线程之间是随机调度的,导致此处的调度顺序充满其他可能性。
线程安全的原因
- 【根本原因】抢占式执行,随机调度
- 代码结构:多个线程同时修改一个变量。
- 原子性:如果修改操作是原子的,影响不是很大。但是如果是非原子的,出现问题的概率就会增加很多。
- 内存可见性问题
- 指令重排序(本质上是编译器优化出现了bug):单个线程里,顺序发生调整。
- …
要想解决线程安全问题,主要手段就是从原子性入手,把非原子的操作,变成原子的。加锁。
解决线程不安全问题(加锁)
上面我们说到。通过加锁,我们可以把不是原子的,转成原子的。
加了synchronized之后,进入add方法就会加锁,出了add方法就会解锁。
如果两个线程同时尝试加锁,此时只有一个能获取锁成功,另一个只能阻塞等待(BLOCK)。一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功。
加锁,说是保证原子性,但并不是说让这里的三个操作一次性完成,也不是这三步操作过程中不进行调度,而是让其他也想操作的线程阻塞等待。加锁本质上是把并发变成了并行。
加锁操作会影响程序的速率,在实际过程中我们要通过实际情景来对其进行合理加锁。
synchronized使用方法:
- 修饰方法
(1)修饰普通方法(锁对象是this)
(2)修饰静态方法(锁对象是类对象(Counter.class))- 修饰代码块(显示/手动指定锁对象)
加锁,要明确执行对哪个对象进行加锁的。
如果两个线程针对同一个对象加锁,会产生阻塞等待。(锁竞争/锁冲突)
如果两个对象针对不同对象加锁,不会参数阻塞等待。(不会锁竞争/锁冲突)
一定要注意锁对象是哪个!
synchronized关键字-监视器锁monitor lock
监视器锁也就是synchronized。有时会在异常中看到这个词。
synchronized的特性
- 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
理解 “阻塞等待”: 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁,就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁。
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
- 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
也就是说,一个线程针对同一个对象,连续加锁两次,是否会有问题。如果没问题就叫做可重入,如果有问题就叫做不可重入的。
上述代码中,锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁。进入add方法时,又遇到了代码块,再次尝试加锁。站在this的角度看(锁对象),自己已经被另外的线程占用了,第二次的加锁是否要阻塞等待呢?
如果允许上述操作,这个锁就是可重入的。如果不允许,就是不可重入的。就会产生死锁
java中的死锁问题
死锁
程序中一旦出现死锁,就会导致线程崩溃了(无法继续执行后续工作)。程序就会产生严重的bug。死锁一般是非常隐蔽的。
死锁的三个典型情况
- 一个线程,一把锁,连续加锁两次。如果锁是不可重入锁,就会死锁
java中synchronized和ReentrantLock都是可重入锁。 - 两个线程两把锁,t1和t2各自先针对锁A和锁B加锁。再尝试获取对方的锁。
举个例子:某人把家里钥匙锁在了车里,把车钥匙锁在了家里;小红写完了英语作业,想要抄小兰的数学作业,小兰写完了数学作业,想要抄小红的英语作业。但是两人都不开口。这个场景就僵住了~
public class ThreadDemo13 {
public static void main(String[] args) {
Object yingyu = new Object();
Object shuxue = new Object();
Thread xiaohong = new Thread(() -> {
synchronized (yingyu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (shuxue) {
System.out.println("小红抄到了小兰的数学作业!小红写完了所有作业。");
}
}
});
Thread xiaolan = new Thread(() -> {
synchronized (shuxue) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (yingyu) {
System.out.println("小兰抄到了小红的英语作业!小兰写完了所有作业。");
}
}
});
xiaohong.start();
xiaolan.start();
}
}
通过运行结果可知,此时没有线程拿到两把锁。
BLOCK表示获取锁,获取不到阻塞状态。
java:16行
java:32
针对这样的死锁问题,需要借助jconsole这样的工作来进行定位,看线程的状态和调用栈。就可以分析代码再哪里死锁了。
- 多个线程多把锁。
经典案例:哲学家就餐问题
每个哲学家有两种状态:
1.思考人生(相当于线程阻塞状态)
2.拿起筷子吃面条(相当于线程获取到所然后执行一些计算的状态)
由于系统的随机操作,这五个哲学家,随时都可能想吃面条,也随时都有可能要思考人生。
想要吃面条就需要拿起左手和右手两根筷子。
假设出现了极端情况就会死锁,即同一时刻,所有哲学家同时拿起左手的筷子,都要等待右边的哲学家把筷子放下。
那么如何解决这个问题呢?
编号(给筷子编号)。先拿编号小的,再拿编号大的。
这样一来,A先拿1,B先拿2,C先拿3,D先拿4,E先拿1。此时E就必须等待A使用完1,再拿起5吃面。E吃完D使用5和4吃…
这样就解决了死锁问题。
public class ThreadDemo13 {
public static void main(String[] args) {
Object yingyu = new Object();
Object shuxue = new Object();
Thread xiaohong = new Thread(() -> {
// 假设 yingyu 是 1 号, shuxue 是 2 号, 约定先拿小的, 后拿大的.
synchronized (yingyu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (shuxue) {
System.out.println("小红抄到了小兰的数学作业!小红写完了所有作业。");
}
}
});
Thread xiaolan = new Thread(() -> {
synchronized (yingyu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (shuxue) {
System.out.println("小兰抄到了小红的英语作业!小兰写完了所有作业。");
}
}
});
xiaohong.start();
xiaolan.start();
}
}
死锁的四个必要条件
- 互斥使用:线程1拿到了锁,线程2就等待。
- 不可抢占:线程1拿到锁之后,必须是线程1主动释放,线程2不能强制获取。
- 请求和保持:线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的。
- 循环等待:线程1尝试获取到锁A和锁B 线程2尝试获取到锁B和锁A;线程1在获取B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放锁A;
如何避免死锁?
如何避免死锁?(以循环等待为突破口)
方法:给锁编号,然后指定一个固定的顺序(从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除。这是解决死锁最简单可靠的办法。
Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
- String