JavaEE初阶——线程安全(多线程)
线程安全
- 线程安全概念
- synchronized锁
- 锁的使用方式
- 锁的互斥和可重入
- 死锁问题
- volatile 关键字
- wait和notify
线程安全概念
可以简单理解为,如果多线程环境下,其代码的运行结果是符合我们预期结果,其可以认为其是线程安全的
例如:在多线程下计算50000 + 50000
public class demo13 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
像上面这个代码,我们使用线程来对同一个变量修改,理想的结果是100000,但是真实的运行结果如下,并且结果不是唯一的
上面这个代码之所以结果不是100000,是因为其1.操作系统调度随机,2.在多线程下运行,3.count++操作不是原子性的
为了让其结果符合操作,一半都是通过3.让其操作变成原子性的即可
原本的count++可以大概分为下面三个指令
1.从内存把数据读到CPU (load)
2. 进⾏数据更新 (add)
3. 把数据写回到CPU (save)
因为其count++对应多个指令不是原子性的,所以在这个多线程下运行结果不符合我们要求
因为一个线程的修改还没有load可能被另一个线程覆盖,这样就会导致一些count无效
那这个结果可能<50000吗?
其实是可以的,可能一个线程执行中间,另一个线程执行了多次"无效"相加
线程不安全的原因
1.操作系统对于线程调度是随机的
2.多线程下修改共享资源(同一变量)
3.操作不是原子性,操作系统是以指令来执行的,一个代码语句可能对应多个指令
4.内存可见性
5.指令重排序
synchronized锁
因此可以使用synchronized加锁,让其操作变成原子性的
public class demo13 {private static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
锁的使用方式
修改时代码块,指定锁对象
任意对象
public class demo13 {private static int count = 0;private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
当前对象
使用this
class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public void add() {synchronized (this) {count++;}}
}
public class demo14 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();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();t1.join();t2.join();System.out.println(counter.count);}
}
通过反射,使用类对象
class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public void add() {synchronized (demo13.class) {count++;}}
}
对方法加锁
class Counter {public int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public synchronized void add() {count++;}
}
对静态方法加锁
class Counter {public static int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public synchronized static void add() {count++;}
}
上面这些都是可以解决上面线程安全问题的,但是这里都是对同一个对象加锁,两个线程对同一个对象加锁,才会产生阻塞等待,如果两个线程分别对应两把不同的锁,此时不会发生竞争,线程安全问题也不会解决
两个线程两把锁,其线程安全问题仍然没有解决
一个加锁,另一个不加,仍然存在线程安全问题
总结
1.进入{是加锁,对应的}表示解锁
2.加锁只是防止其他线程插队,并不影响线程调度
3.锁对象,两个线程针对统一对象加锁,才会有锁竞争,反之不会
锁的互斥和可重入
互斥
synchronized会起到互斥的效果,某个线程执行到了这个synchronized中时,其他线程如果也执行到同一对象的锁,就会阻塞等待
进入synchronized修改的代码块,相当于加锁
退出synchronized修改的代码块,相当于解锁
可重入
java中锁是可重入的,对同一对象加两次锁不会出现锁死情况
class Counter {public static int count = 0;// 把 synchronized 加到实例方法上, 此时就是给 this 加锁.public void add() {synchronized (this){count++;}}
}
public class demo14 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {//连续加同一把锁synchronized (counter){synchronized (counter){counter.add();}}}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {synchronized (counter){counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}
虽然这里对同一线程连续加了同一把锁,但是java中会自行判断,因此这里只有第一次是加锁成功,后面其会判断是否和第一次加锁是同一线程、同一把锁,如果是同一线程,第二次加锁,相当于”直接跳过“
死锁问题
1.一个线程一把锁,但是连续加锁多次,java中可重入锁,解决了这个问题
2.两个线程两把锁(可能会出现你等我,我等你的问题,像门钥匙锁车里了,车钥匙锁家里了)
3.n个线程,m把锁
两个线程两把锁
//两个线程两把锁
public class demo18 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (locker1){System.out.println("t1获取locker1");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker2){System.out.println("t2获取locker2");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2获取locker1");}}});t1.start();t2.start();}
}
这是两个线程两把锁,此时当t1获取到locker1,t2获取到locker2后,出现了问题,但是此时两个锁都没有释放,但是在t1线程中locker1锁中又有获取locker2锁,在t2线程中locker2锁中又有获取locker1锁,但此时要获取锁,必须要等到锁释放,但是这里释放锁,又要需要你获取锁,就这样无线循环,导致线程锁住了
在jdk中bin目录下jconsole.exe文件中可以发现,这两个线程的状态是BLOCKED,此时都是因为锁导致阻塞
上面这个问题,其实我们可以让其按照一定的顺序进行加锁,规定都是先获取locker1,在获取locker2
public class demo18 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (locker1){System.out.println("t1获取locker1");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker1){System.out.println("t2获取locker2");try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2获取locker1");}}});t1.start();t2.start();}
}
在这个场景下,可以使用这种方法解决这个问题
n个线程,m把锁
科学家就餐问题
出现死锁的四个必要条件
1.锁的基本特性,锁是互斥的
2.锁不可被抢占,A获取到locker,当A线程还没有释放的的时候,B把locker锁抢过来了,导致A线程阻塞
3.请求 和 保持 A线程持有locker1锁,还没有释放,但是又开始获取locker2
4.循环等待,门钥匙锁车里了,车钥匙锁家里了
如何避免呢?
1.避免请求和保持,也就是要避免锁的嵌套
2.打破循环等待,按照顺序进行加锁,如果一个线程有多把锁,可以进行编号,让其从小到大顺序进行加锁
volatile 关键字
volatile修饰的变量可以保证”内存可见性“
public class demo15 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}
我们输入一个非0元素,其t1线程的循环会结束,但是这里并没有结束
内存可见性问题其实是由编译器优化导致的
由javac将其 .java文件 => .class文件
jvm文件执行.class文件
再java中,由工作内存和主内存
主内存可以看成内存
其工作内存可以看成 寄存器 / 缓存
因此可以使用volatile修饰这里的flag变量,这里就会打破上面的优化,这里加上这个修饰,其就会还是从内存中读取值放入寄存器/缓存中,这样就不会出现上面的问题,但是这里虽然准确了,但是其速度变慢了
public class demo15 {private volatile static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}
使用sleep
public class demo15 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (flag == 0){//这里什么都不做try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1 线程结束");});Thread t2 = new Thread(() ->{Scanner sc = new Scanner(System.in);System.out.println("请输入非0数结束线程t1");flag = sc.nextInt();System.out.println("t2结束");});t1.start();t2.start();}}
为什么这里使用sleep也会是正确的呢,其sleep并不是可以解决内存可见性,而是因为sleep对应的指令非常多,比上面的load还多,所以这里编译器就不会优化掉
wait和notify
因为多线程下,线程调度是随机的,但是我们其实并不希望其是随机的,我们想要确定其线程调度顺序,因此这里就要使用wait和notify这两个方法
方法 | 说明 |
---|---|
public final void wait() | 让线程进入等待,等到notify结束 |
public final native void wait(long timeoutMillis) | 有时间限制等待,精确到毫秒 |
public final void wait(long timeoutMillis, int nanos) | 同理,精确到纳秒 |
wait()方法
1.是当前线程执行的代码进入等待
2.释放当前锁
3.满足一定条件唤醒当前锁(notify),会重新获取锁
这里的wait方法要结合synchronized锁来使用,否则会报错
唤醒方式
1.使用该对象的notify方法唤醒
2.如果是时间限制wait方法,就等到时间限制结束也可以唤醒
3.或者暴力方法,调用interrupted方法,抛出InterruptedException异常
方法 | 说明 |
---|---|
public final native void notify() | 唤醒一个wait状态的线程 ,如果有多个就随机唤醒一个 |
public final native void notifyAll() | 唤醒全部wait等待的线程 |
使用notify()方法唤醒wait()线程等待,让其重新获取该对象锁
但是这里要执行完notify()方法中的所有逻辑,结束以后才释放锁,这样wait()才会开始重新获取锁
public class demo16 {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized(locker){System.out.println("t1 wait之前");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait之后");}});Thread t2 = new Thread(() ->{synchronized (locker){System.out.println("notify 之前");Scanner sc = new Scanner(System.in);System.out.println("请输入任意内容,触发notify");sc.nextInt();locker.notify();System.out.println("notify 之后");}});t1.start();t2.start();}
}
运行结果如下
唤醒所有wait
public class demo17 {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() ->{synchronized (locker){System.out.println("t1开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1结束");}});Thread t2 = new Thread(() ->{synchronized (locker){System.out.println("t2开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2结束");}});Thread t3 = new Thread(() ->{synchronized (locker){System.out.println("t3开始");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t3结束");}});Thread t4 = new Thread(() ->{synchronized (locker){System.out.println("t4开始");Scanner sc = new Scanner(System.in);System.out.println("输入任意,唤醒wait");sc.next();locker.notifyAll();System.out.println("t4结束");}});t1.start();t2.start();t3.start();t4.start();}
}
此时如果使用notify,这时候只会随机唤醒一个线程,并不会唤醒所有,程序也不会结束
wait和sleep区别
相同:都可以让线程等待,都可以设置最大等待时间
都可以被interrupt唤醒,但是wait更希望被notify唤醒,而sleep和interrupt,这可能会导致线程直接中止
1.wait需要搭配synchronized 使用,而sleep不需要
2.wait是Object的方法,sleep是Thread静态方法
3.wait是为了被notify,等待时间只是最后才会使用,而sleep就是休眠一定时间
4.wait会先释放锁,再获取锁,而sleep休眠不会释放锁