java线程安全问题的原因和解决方案

本文详细介绍了线程不安全的原因,包括抢占式执行、多个线程共享变量、非原子操作、内存可见性和指令重排序。解决线程安全问题主要依赖于synchronized关键字,通过互斥、内存刷新和可重入特性确保线程安全。此外,volatile关键字用于保证内存可见性,但不保证原子性。wait和notify方法则用于线程间的通信与协调。文章深入探讨了这些概念并提供了实例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

造成线程不安全的原因有哪些呢?

  • 抢占式执行,调度过程随机(也是万恶之源,无法解决)
  • 多个线程同时修改同一个变量(可以适当调整代码结构,避免这种情况)
  • 针对变量的操作,不是原子的(加锁,synchronized)
  • 内存可见性,一个线程频繁读,一个线程写(使用volatile)
  • 指令重排序(使用synchronized)

解决方法:

1)使用synchronized关键字以及synchronized的基本使用

synchronized的本质操作,是修改了Object对象中的“对象头”里面的一个标记,只有当两个线程同时针对一个对象加锁,才会产生竞争

把synchronized加到普通的方法上,也就相当于把锁对象指定为this了

 

把synchronized加到代码块上,就需要手动锁定,针对那个对象加锁

 把synchronized加到静态方法上,所谓的“静态方法”更严谨的叫法,应该叫做“类方法”,普通的方法,更严谨的叫法,叫做”实例方法“?,针对类对象加锁如下:

2)synchronized的特性

1.互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

2.刷新内存~synchronized 的工作过程
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁

3.可重入

死锁的四个必要条件:

  • 互斥使用~ 一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
  • 不可抢占~ 一个锁被一个线程占用了之后,其他线程不能把这个锁给抢走
  • 请求与保持~ 当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁是始终都是被该线程所持有的
  • 环路等待,等待关系(为避免环路等待,只需要约定好,针对多把锁加锁的时候,有固定的顺序即可)

2)volatile关键字

  • volatile保证内存可见性,禁止编译器优化
  • volatile只是处理一个线程读,一个线程写的情况
  • volatile不保证原子性,也不会引起线程阻塞

3)wait和notify

由于线程之间是抢占式执行的,因此线程之间执行的顺序难以预知,因此我们需要合理的协调多个线程之间的执行先后顺序

完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

1,wait()方法

wait 做的事情:

public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

1.其他线程调用该对象的 notify 方法.

2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

3.其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

2,notify()方法:

notify 方法是唤醒等待的线程.

static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}

1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

2.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")

3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
完,也就是退出同步代码块之后才会释放对象锁。

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

wait和sleep的对比(简单了解即可)

wait用于线程之间的通信的,sleep是让线程阻塞一段时间

唯一的相同点就是都可以让线程放弃执行一段时间.

1. wait 需要搭配 synchronized 使用. sleep 不需要.

2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个学编程的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值