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线程执行AddA寄存器的值变为4,在内存中count = 3

4.A线程执行WriteA寄存器的4写入到内存中,此时内存中的count = 4

5.B线程执行AddB寄存器的值变为4,此时内存中count = 4

6.B线程执行WriteB寄存器的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++这个操作变成了原子操作(LoadAddWrite 会一次性执行完毕)。

下面是锁竞争的一个具体过程:

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线程修改flagA线程应该会停止执行,但是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()方法就是使线程停止运行

  1. wait()方法的作用是使当前代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入"预执行队列"中,并且在wait()所在的代码出停止执行,直到接到通知或被中断为之.
  2. wait()方法只能在同步方法中或同步块中调用.如果调用wait()方法时,没有持有适当的锁,就会抛出异常
  3. 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的一小段现象.具体总结如下:

  1. wait()方法执行前需要请求锁,而wait()执行时会先释放锁,等被唤醒时再重新请求锁,这个锁是wait()对象上的monitor lock.
  2. sleep()方法是无视锁的存在的,即之前请求的锁不会释放,即使没有锁也不会去请求锁.
  3. wait()方法是Object的方法.
  4. sleep()方法是Thread的静态方法.

以上是关于JavaEE几个线程不安全的原因和解决方法的主要内容,如果未能解决你的问题,请参考以下文章

Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

java线程锁策略

Java——多线程高并发系列之ArrayListHashSetHashMap集合线程不安全的解决方案

Java——多线程高并发系列之ArrayListHashSetHashMap集合线程不安全的解决方案

多线程之 线程安全