多线程
二、线程安全
1.观察线程不安全
java
public class demo11 {
static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
thread1.start();
thread2.start();
//两个join,防止线程coun++操作还未执行完就打印
thread1.join();
thread2.join();
System.out.println(count);
}
}
不断执行代码发现,结果不一定,有时小于预期结果10000
java
thread1.start();
thread1.join();
thread2.start();
thread2.join();
将代码改为串行执行,先执行一个再执行另一个,就不会出现错误情况
所以,很明显上面的代码就是因为多线程的并发执行而造成的bug,这就称之为"线程安全"。
从cpu上角度,对于count++代码对应3条cpu指令1)load:把内存中count的值,加载到cpu寄存器
2)add:把寄存器的内容加一
3)save:把寄存器的内容保存回到内存中
操作系统对于线程的调度是随机的,执行这3个指令,不是一口气执行完,很可能执行到一部分就被调度走了,例如thread1刚执行完add还未save操作系统就就执行thread2的load了。
预期:
可能出现情况:
当一个线程运行时但还未运行到save把增加的值存回内存,另一个内存就把还未增加的值加载到寄存器就会出现错误。
2.线程安全问题产生原因
- 1)根本原因就是随机调度,抢占式执行
- 2)多个线程同时修改同一个变量
- 3)修改操作不是原子的
如果修改操作只是对应一个cpu指令就是原子的,例如=操作,而上面例子就不是原子的 - 4)内存可见性
- 5)指令重排序
指令重排序是指编译器和处理器为了优化性能,可能会在不改变单线程程序执行结果的前提下,重新安排指令的执行顺序。但在多线程环境下,这可能导致意外的结果。
3.线程安全问题解决方案
对于产生原因随机调度,抢占式执行,是系统的设定无法改变;对于原因多个线程同时修改同一个变量,这种操作也是有些情况必须需要的例如某个商品订单创建量不能超出库存;所以我们只能将操作打包为原子操作。
重要线程安全操作:加锁
通过加锁,让不是原子的操作打包为原子操作,一个线程运行这个操作时,此线程锁死在cpu上,其他线程只能阻塞等待,不允许插队
synchronized加锁的代码使用:
java
synchronized(){//进入代码块相当于加锁
//执行要保护的逻辑
}//出代码块相当于解锁
()中可以写任何一个对象,但多个线程针对同一个对象加锁才会产生互斥效果
在count++代码使用加锁操作:
java
public class demo11 {
static int count=0;
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (object) {
count++;
}
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (object) {
count++;
}
}
});
thread1.start();
thread1.join();
thread2.start();
//两个join,防止线程coun++操作还未执行完就打印
thread2.join();
System.out.println(count);
}
}
synchronized的变种写法
可以用synchronized修饰方法
java
synchronized (this) {
count++;
}
变形:
java
synchronized public void add(){
count++;
}
特殊情况:synchronized修饰static方法,不存在this相当于针对类对象进行加锁
java
public static void add(){
synchronized (counter.class){//类对象
count++;
}
}
java
public synchronized static void add(){
count++;
}
用synchronized修饰方法对count++代码使用加锁操作(将修改count的操作封装成原子操作解决线程安全问题):
java
class counter{
static int count=0;
public void add(){
synchronized (this) {
count++;
}
}
public int get(){
return count;
}
}
public class demo11 {
public static void main(String[] args) throws InterruptedException {
counter counter1=new counter();
Thread thread1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter1.add();
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter1.add();
}
});
thread1.start();
thread1.join();
thread2.start();
//两个join,防止线程coun++操作还未执行完就打印
thread2.join();
System.out.println(counter1.get());
}
}
死锁
1.一个线程一把锁,连续加锁两次,synchronized的可重性
java
public class demo13 {
//死锁-一个线程一把锁->可重入
public static void main(String[] args) {
Object object=new Object();
Thread thread=new Thread(()->{
synchronized (object){
synchronized (object){
System.out.println("thread线程执行");
}
}
});
thread.start();
}
}
从代码上看第二次进行加锁,此时锁对象已经是被占用状态,会发生阻塞等待,想要往下执行,就需要等待第一次的锁被释放,这样的问题就叫死锁 。但java中上面的代码并不会阻塞,就是因为java的synchronized的可重入概念,即当某个线程针对一个锁加锁后,后续这个线程再次针对这个锁加锁就不会阻塞,而是继续执行。
原理:让锁对象进行内存保护,是哪个线程持有该锁,后续线程再次针对这个线程加锁时判断,锁持有者线程和当前加锁的线程是否是同一个,是同一个继续执行,不是则阻塞等待。
如何自己实现一个可重入锁?
1.在锁内部进行记录当前是哪个线程持有该锁,后续每次加锁,进行判定
2.通过计数器,记录当前线程加锁的次数,从而判断什么时候解锁
2.两个线程两把锁,每个线程进行加锁后尝试获取对方的锁
java
public class demo12 {
//死锁-两个线程两把锁
public static void main(String[] args) throws InterruptedException {
Object object1=new Object();
Object object2=new Object();
Thread thread1=new Thread(()->{
synchronized (object1){
try {
Thread.sleep(1000);//不加sleep可能不会出现死锁,可能thread1拿到了两把suo
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object2){
System.out.println("thread1两个锁都获取到了");
}
}
});
Thread thread2=new Thread(()->{
synchronized (object2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object1){
System.out.println("thread2两个锁都获取到了");
}
}
});
thread1.start();
thread2.start();
Thread.sleep(1000);
System.out.println(thread1.getState());//BLOCKED
}
}
3.M个线程N把锁(哲学家就餐问题)每个线程持有一把锁都尝试获取下一个线程的锁,形成死循环
java
public class demo14 {
//N个线程M把锁
public static void main(String[] args) {
Object object1=new Object();
Object object2=new Object();
Object object3=new Object();
Object object4=new Object();
Thread thread1=new Thread(()->{
synchronized (object1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object2){
System.out.println("thread1执行中");
}
}
});
Thread thread2=new Thread(()->{
synchronized (object2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object3){
System.out.println("thread2执行中");
}
}
});
Thread thread3=new Thread(()->{
synchronized (object3){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object4){
System.out.println("thread3执行中");
}
}
});
Thread thread4=new Thread(()->{
synchronized (object4){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object1){
System.out.println("thread4执行中");
}
}
});
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
那如何避免死锁呢
锁的四个必要条件:
1.锁是互斥的 ,一个线程拿到一个锁后,另一个线程要获取这把锁必须阻塞等待
2.锁是不可抢占的 ,线程1拿到锁,线程2也尝试获取这把锁阻塞等待而不能抢占
1,2都是锁的基本特性,无法改变
3.请求和保持 ,一个线程获取锁1后,在不释放锁1情况下尝试获取锁2
对于3,我们可以将嵌套的锁改为并列锁
4.循环等待 ,多个线程多把锁之间的等待过程,构成了循环
对于4,我们可以在每个线程加锁是,永远先获取序号小的锁,后获取序号大的
volatile关键字-解决内存可见性问题
volatile修饰的变量,能保证内存可见性
每个线程,有一个自己的"工作内存"(可理解为寄存器加L1,L2,L3缓存)同时这些线程共享同一个"主内存",当一个线程循环进行上述读取变量操作时,就会把主内存中数据拷贝到工作内存中,后续另一个线程修改,也是先修改自己的工作内存,拷贝到主内存中,由于第一个线程仍然读自己的内存,因此感知不到主内存的变化。
java
public class demo15 {
//内存可见性
volatile static int flag=0;//这样变化的读取操作,就不会被编译器进行优化
public static void main(String[] args) {
Thread t=new Thread(()->{
while (flag==0){
}
System.out.println("t线程结束");
});
Thread t1=new Thread(()->{
Scanner sc=new Scanner(System.in);
System.out.println("请输入flag的值");
flag=sc.nextInt();
});
t.start();
t1.start();
}
}
我们可以在while中加入sleep,,sleep消耗的时间远远大于load flag的时间这样,优化flag到工作内存的操作也就没有意义了,但是在实际中sleep也会大大降低了程序的效率。
此时加入volatile修饰变量,此时编译器对这个变量的读取操作,就不会优化成工作内存,必须重新读取内存的数据,避免优化带来的bug ,上述事例t1线程修改flag存入内存,就可以被t线程读取到,从而结束t线程。
volatile对于原子性问题无用。
4.wait和notify-协调线程之间执行逻辑的顺序
可以让后执行的线程逻辑让先执行的线程逻辑跑完,再通知他继续执行,对于jion只能让一个线程彻底执行完,那一个线程才能执行,wait也是等,但,等到另一个线程执行完notify,就可以继续走。
wait方法
(必须搭配sychronized,否则抛出异常)
用处:
- 使当前执行代码的线程进行等待(把线程放在等待队伍中)
- 释放当前锁(join不会)
- 满足条件后重新获取这个锁
wait等待结束条件
- 其他线程调用改同一个对象的notify方法
- wait等待时间超时
- 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常
notify方法
(必须搭配sychronized,否则抛出异常)
用处:
- 通知等待该对象的对象锁的其他线程,让他们重新获取该对象的对象锁
- 如果多个线程等待,线程调度器会随机挑选一个,不是先来后到
- 要等到notify的代码执行完,才会释放对象锁
代码实例:
(要确保是同一个对象,notify必须在wait之后)
java
public class demo19 {
public static void main(String[] args) {
Object object=new Object();
Thread thread1=new Thread(()->{
synchronized (object){
try {
System.out.println("wait方法开始");
object.wait();
System.out.println("wait方法结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread2=new Thread(()-> {
synchronized (object) {
System.out.println("输入任意内容唤醒thread1");
Scanner sc=new Scanner(System.in);
sc.next();
object.notify();
System.out.println("notify方法结束");
}
});
thread1.start();
thread2.start();
}
}
运行结果:

notifyAll方法
用处:一次性唤醒所有等待的线程
但是只是让这些锁重新进行竞争,只有某个线程先加上锁,其他阻塞等待
wait与sleep对比
相同:都是让线程放弃执行一段时间
不同:
- wait必须搭配synchronized使用,sleep不用
- wait是object的方法,sleep是Thread的静态方法