0
点赞
收藏
分享

微信扫一扫

【JavaEE初阶】线程安全问题及解决方法

ixiaoyang8 2023-11-25 阅读 48

目录

一、多线程带来的风险-线程安全

1、观察线程不安全

2、线程安全的概念

3、线程不安全的原因

4、解决之前的线程不安全问题 

5、synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

5.2 synchronized 使用示例  

5.3 Java 标准库中的线程安全类 


一、多线程带来的风险-线程安全

1、观察线程不安全

public class ThreadDemo2 {
private static long count = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
for (int i = 1;i <= 500000;i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (long i = 0;i < 500000;i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//存在线程安全问题,输出的结果可能不准确
System.out.println("count= "+count);
}
}

其运行结果:

明显这个结果和我们的预期是不一样的,这是因为存在线程安全问题。若把count++的操作在一个单线程环境下运行 ,便不会出现这样的问题。下面我们来说一下线程安全问题。

2、线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:

3、线程不安全的原因

  • 线程调度是随机的
  •  修改共享数据

上面的线程不安全的代码中,涉及到多个线程针对 count 变量进行修改, 此时这个 count 是⼀个多个线程都能访问到的 "共享数据" 。

  • 原子性  

什么是原子性

⼀条 java 语句不⼀定是原子的,也不一定只是一条指令

比如,刚才我们看到的 count++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU 寄存器中
  2. 进行数据更新
  3. 把数据写回到内存

那么不保证原子性会给多线程带来什么问题呢?

  •  可见性

 可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到。这里先不过多介绍。

  • 指令重排序 

什么是代码重排序?

假设有⼀段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑⼀次前台。这种叫做指令重排序

重排序是⼀个比较复杂的话题, 涉及到 CPU 以及编译器的⼀些底层⼯作原理, 此处不做过多讨论。 

4、解决之前的线程不安全问题 

 解决之后的代码:

public class ThreadDemo2 {
private static long count = 0;
public static void main(String[] args) throws InterruptedException{
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 1;i <= 500000;i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(()->{
for (long i = 0;i < 500000;i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("count= "+count);
}
}

这时的结果就一定是1000000,如图:

下面就给大家解释一下,这个线程不安全的问题是如何解决的。

5、synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥  

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同⼀个对象 synchronized 就会 阻塞等待

  • 进入 synchronized 修饰的代码块,相当于 加锁

  • 退出 synchronized 修饰的代码块,相当于 解锁

synchronized用的“锁”是存在Java对象“头”里面的。

 

 synchronized的底层是使用操作系统的mutex lock实现的。

2) 可重入

synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题; 

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题。

for (int i = 0; i < 50000; i++) {
synchronized (locker) {
synchronized (locker) {
count++;
}
}
}

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

5.2 synchronized 使用示例  

synchronized 本质上要修改指定对象的 "对象头",从使用度来看,synchronized 也势必要搭配⼀个具体的对象来使用。

 1) 修饰代码块: 明确指定锁哪个对象.

 锁任意对象:

public class SynchronizedDemo {
private Object locker = new Object();

public void method() {
synchronized (locker) {

}
}
}

锁当前对象:

public class SynchronizedDemo {
public void method() {
synchronized (this) {

}
}
}

2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象 

public class SynchronizedDemo {
public synchronized void methond() {

}
}

3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象 

public class SynchronizedDemo {
public synchronized static void method() {

}
}

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产生阻塞等待.

两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.

5.3 Java 标准库中的线程安全类 

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:

但是还有⼀些是线程安全的. 使用了⼀些锁机制来控制:

StringBuffer 的核心方法都带有 synchronized .  

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的: String

举报

相关推荐

0 条评论