JavaEE几个线程不安全的原因和解决方法
Posted 西伯利亚小土豆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaEE几个线程不安全的原因和解决方法相关的知识,希望对你有一定的参考价值。
1.线程抢占式执行
多个线程抢占式执行,争夺时间片,哪个线程拿到时间片,哪个线程就运行。
这是线程不安全的根本原因,但这个机制操作系统设定的,没办法更改这个机制。
2.多个线程修改同一个变量
多个线程修改同一个变量是很常见的一个线程安全问题,因为线程拿到的数据可能是无效的。
就比如有一个变量count
,线程A和B同时执行count++
,就可能存在问题。
先说count++
这个操作的执行过程:
线程A和线程B同时执行count++
:
假设在内存中count
的值为3,A线程所用的寄存器叫A寄存器,B线程所用的寄存器叫B寄存器。
1.A
线程执行Load
,在内存中 count = 3
,A寄存器的值为 3
。
2.B
线程执行Load
,在内存中 count = 3
,B寄存器的值为 3
。
3.A
线程执行Add
, A
寄存器的值变为4
,在内存中count = 3
。
4.A
线程执行Write
,A
寄存器的4写入到内存中,此时内存中的count = 4
。
5.B
线程执行Add
,B
寄存器的值变为4,此时内存中count = 4
。
6.B
线程执行Write
,B
寄存器的4写入到内存中,此时内存中的count = 4
。
问题就出现了,执行了两次count++
操作,却只增加了1,不符合我们的预期。
🚩example:
让A线程执行5000次count++
,也让B线程执行5000次count++
。
public class Demo1
public static int count = 0;
public static void main(String[] args) throws InterruptedException
Thread A = new Thread(()->
//线程A执行count++,5000次
for (int i = 0; i < 5000; i++)
count++;
);
Thread B = new Thread(()->
//线程B执行count++,5000次
for (int i = 0; i < 5000; i++)
count++;
);
A.start();
B.start();
Thread.sleep(2000);
System.out.println("count = " + count);
🚩结果:
🚩通过加锁,我们就可以使得结果符合我们的预期:
public class Demo1
public static int count = 0;
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException
Thread A = new Thread(()->
for (int i = 0; i < 5000; i++)
//加锁
synchronized (lock)
count++;
);
Thread B = new Thread(()->
for (int i = 0; i < 5000; i++)
//加锁
synchronized (lock)
count++;
);
A.start();
B.start();
Thread.sleep(2000);
System.out.println("count = " + count);
🚩结果:
加锁后使得count++
这个操作变成了原子操作(Load
,Add
,Write
会一次性执行完毕)。
下面是锁竞争的一个具体过程:
3.内存的不可见性
🗻看下面一段代码:
public class Demo2
public static int flag = 1;
public static void main(String[] args) throws InterruptedException
Thread A = new Thread(()->
while (flag == 1)
);
A.start();
Thread.sleep(1000);
flag = 0;
🗻结果:
预期的结果是,main
线程修改flag
,A线程
应该会停止执行,但是A线程
并没有停止执行。
为什么会这样呢?❓
原因是编译器会对我们的代码进行一个优化。
执行flag == 1
的时候本来是下面这三个步骤:
1.从内存中读取flag
的值,存到cpu寄存器
中
2.cpu
执行cmp(compare)
操作
但是,编译器对我们的代码进行了优化,第一次执行flag == 1
完之后,不会从内存中读取flag
的值了。
而是直接拿寄存器的值(第一次读取的flag值)进行比较。
这就叫内存的不可见性,如果不想让编译器优化我们的代码,使得每次都会从内存读取数据,再进行比较。
可以使用volatile
关键字。
volatile的意思:
在创建变量的时候使用volatile关键字:
volatile public static int flag = 1;
再执行代码:
线程A停止执行。
4.指令重排序
编译器优化还会带来指令重排序的问题:
⛅线程A:
Student stu = new Student();
⛅线程B:
while (stu != null)
...
...
...
创建Student
对象的正常顺序:
1.在堆区开辟内存空间
2.执行构造器,对属性初始化
3.将引用赋给stu变量。
编译器优化后:
1.在堆区开辟内存空间。
2.将引用赋给stu变量。
3.执行构造器,对属性初始化。
当线程A执行完2后,线程B可能刚好被调度,执行循环判断部分,然后执行循环体的代码。
这个时候,对象中的属性还没初始化,线程B就有可能拿着这个对象去做事了,就可能出现bug。
如果不想让编译器优化我们的代码,在创建全局变量的时候依然用volatile
关键字修饰。
指令重排序在代码中测试不了,所以这里用文字的方式解读。
Java(高阶)——线程安全
多线程带来的风险
什么是线程安全
有关线程安全的定义是复杂的,但是我们通常可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的.
线程不安全的原因
1.原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人,如果没有任何机制保证,A进入房间之后,还没有出来,B就想进去,打断A在房间里面的隐私,这就不具有原子性,但是我们在A进这个房间之后,给房间加上一把锁,那这样就保证了A的隐私,这就保证了这段代码的原子性.这种现象也叫做同步互斥,表示操作是相互排斥的.
2.可见性
以下是主内存在工作时的示意图:
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题.
3.代码顺序性
一段代码是这样的:
(1).去菜鸟驿站取快递
(2).去图书馆学习10分钟
(3).去菜鸟驿站寄快递
如果是在单线程情况下,JVM,CPU指令集会对其进行优化,比如,按3->1->2的方式执行也是没有问题的,可以少跑一次菜鸟驿站.这叫做指令重排序.
如何解决线程不安全的问题?
1.synchronized关键字-监视器锁monitor lock
synchronized的底层时使用操作系统的mutex lock实现的.
- 当线程释放锁时,JVM会把线程对应的工作内存中的共享变量刷新到主内存中
- 当线程获取锁时,JVM会把线程对应的的本地内存置为无效.从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
- synchronized用的锁是存在Java对象头里的
- synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
//锁的 SynchronizedDemo 对象
public class SynchronizedDemo
public synchronized void methond()
public static void main(String[] args)
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();
// 进入方法会锁 demo 指向对象中的锁;出方法会释放 demo 指向的对象中的锁
public class SynchronizedDemo
public synchronized static void methond()
public static void main(String[] args)
method(); // 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
SynchronizedDemo.class 指向的对象中的锁
public class SynchronizedDemo
public void methond()
// 进入代码块会锁 this 指向对象中的锁;出代码块会释放 this 指向的对象中的锁
synchronized (this)
public static void main(String[] args)
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();
2.volatile关键字
修饰的共享变量,可以保证可见性,部分保证顺序性
class ThraedDemo
private volatile int n;
对象的等待集wait set
wait()方法
wait()方法就是使线程停止运行
- wait()方法的作用是使当前代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入"预执行队列"中,并且在wait()所在的代码出停止执行,直到接到通知或被中断为之.
- wait()方法只能在同步方法中或同步块中调用.如果调用wait()方法时,没有持有适当的锁,就会抛出异常
- wait()方法执行之后,当前线程释放锁,线程与其他线程竞争重新获取锁.
public static void main(String[] args) throws InterruptedException
Object object = new Object();
synchronized (object)
System.out.println("等待中...");
object.wait();
System.out.println("等待已过...");
System.out.println("main方法结束...");
运行结果:
这样在执行到Object.wait()之后就一直等待下去,但是程序肯定不能一直这么等待下去,这个时候就需要使用另一个方法(notify())来唤醒它
notify()方法
notify()方法就是时停止的线程继续执行
- notify()方法也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的那些其他线程,该方法向这些其他线程发出通知notify,并使它们重新获取该对象的对象锁,.如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程
- 在notify()方法之后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁.
- wait(),notify()必须使用在synchronized同步方法或者代码块内.
notifyAll()方法
多个线程在等待就可以用notifyAll()方法一次性唤醒所有的等待线程.
[面试题]:wait()和sleep()的对比
实际上wait()方法与sleep()方法是完全没有可比性的,前者用于线程之间的通信,后者是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间,说白了就是放弃线程执行知识wait的一小段现象.具体总结如下:
- wait()方法执行前需要请求锁,而wait()执行时会先释放锁,等被唤醒时再重新请求锁,这个锁是wait()对象上的monitor lock.
- sleep()方法是无视锁的存在的,即之前请求的锁不会释放,即使没有锁也不会去请求锁.
- wait()方法是Object的方法.
- sleep()方法是Thread的静态方法.
以上是关于JavaEE几个线程不安全的原因和解决方法的主要内容,如果未能解决你的问题,请参考以下文章
Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法
Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法
Java——多线程高并发系列之ArrayListHashSetHashMap集合线程不安全的解决方案