多线程带来的的风险-线程安全
1.线程不安全的原因
• 线程调度是随机的 这是线程安全问题的罪魁祸首
例如:修改共享数据
public class test15 {
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= "+count);
}
}
运行结果:
这是一个典型的线程不安全的例子,上述代码涉及到多个线程针对count变量进行修改,此时这个 count 是⼀个多个线程都能访问到的 “共享数据”,按理来说上述代码的执行结果应该是100000,那为什么结果是随机的呢?
这是因为一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++,其实是由三步操作组成的:1. 从内存把数据读到 CPU 2. 进⾏数据更新 3. 把数据写回到 CPU。
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。这也就导致了count每次执行的结果是随机的。
2.多线程引起线程安全原因(实质是造成了读写不一致)
-
当多个线程操作共享空间中的变量时,就有可能造成线程安全问题(如一个线程更新变量之前,另一个线程读到了旧值并已经更新了,导致该线程再去更新时,更新的值相对来说就不正确了)
-
结合内存空间的共享性,也就是说,当多个线程同时操作堆区中对象的成员变量,或者方法区中的静态变量时,就会造成线程安全问题
3.深入理解为什么线程之间会造成读写不一致(四个原因)
首先线程并发导致安全问题的根本原因主要有4个:
**(1). 多线程调度的随机性(抢占式执行):**由于多个线程是 “抢占式执行的” , 所以造成了多线程调度的随机性, 无序性
这是导致多线程环境下线程不安全的最根本原因
(2). 原子性:线程切换会带来原子性问题,使用锁即可解决。java中只有简单的赋值操作,如i = 100是原子性操作,但是i = j则不是
(3). 可见性:由于cup高速缓存的存在,可能会导致线程对一个变量修改没有及时被其他线程所看见,使用volatile关键字即可解决
(4). 指令重排序:jvm会对代码进行优化,从而会把代码进行重排序,使用volatile关键字可以禁止重排序
4.解决线程安全问题的思路(同时满足原子性,可见性与有序性)
1.避免线程修改共享空间中变量的值
2.使用无状态对象,即不共享状态(数据)给多个线程
3.使用不可变对象,不可修改,就不会存在读写不一致的问题
4.使用线程特有对象,如TheadLocal
5.装饰者模式,即使用原子类,原子操作
6.使用锁,保证线程同步,如Syconized,RetranceLock等
5.解决多线程安全synchronized关键字的特性和用法
特性:synchronized关键字 最主要的特性就是 : 互斥性
synchronized 对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;
synchronized 是重量型锁
用法:synchronized关键字用于对共享资源进行加锁,保证同一时间只有一个线程可以访问被加锁的代码块或方法。当一个线程进入synchronized代码块时,它会尝试获取锁,如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。这样可以确保在任意时刻只有一个线程能够执行被加锁的代码,从而避免了多线程并发访问共享资源时的数据冲突问题。
6.解决多线程安全volatile关键字
volatile只能保证可见性与有序性,不能保证原子性
volatile关键字的作用主要有如下两个:
1.保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2.保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
private static volatile int count=0;
public static void main(String[] args) {
Thread t=new Thread(()->{
while(count==0){
}
System.out.println("t执行~");
});
t.start();
Scanner scanner=new Scanner(System.in);
System.out.println("请输入:");
count=scanner.nextInt();
}
}
那么volatile是如何保证可见性与有序性的呢?
java内存模型分为线程的工作内存与主内存。主内存则会存放着一些共享变量;工作内存则是每一个线程独有的。当要操作主内存的变量时,线程会先从主内存中复制一份缓存到自己的工作内存,然后在自己的工作内存对值进行修改,之后再把值更新到主缓存中。因此当有一些线程事先缓存了变量或者线程修改的变量没有及时更新到主内存中,就会导致线程安全问题
volatile为何不能保证原子性呢?
java中只有简单的赋值才是原子性操作,所以volatile并不能保证原子性
7.解决多线程安全wait 和 notify关键字
- wait做的事情
• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
• 释放当前的锁
• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
- wait 结束等待的条件
• 其他线程调⽤该对象的 notify ⽅法
• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间)
locked2.wait(200); // 毫秒单位
• 其他线程调用该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常
如果这三个条件都没有,则该线程会进入死等状态
- notify 方法是唤醒等待的线程
• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其 它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 “先来后到”),但是也可以使用不同锁对象来选择要唤醒的线程
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏ 完,也就是退出同步代码块之后才会释放对象锁
public class test13 {
public static void main(String[] args) {
Object locked1=new Object();
Object locked2=new Object();
Object locked3=new Object();
Thread a=new Thread(()->{
synchronized (locked1){
try {
locked1.wait(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("a");
}
});
Thread b=new Thread(()->{
synchronized (locked2){
try {
locked2.wait(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("b");
}
});
Thread c=new Thread(()->{
synchronized (locked3){
try {
locked3.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("c");
}
});
Thread d=new Thread(()->{
synchronized (locked3){
locked3.notify();
}
});
a.start();
b.start();
c.start();
d.start();
}
}
上述代码实现了使用不同锁对象来选择要唤醒的线程,创建3个不同锁对象使用notify()选择唤醒线程c