线程安全问题的原因
- 修改共享数据:当多个线程尝试同时修改同一个共享数据时,可能会引发线程安全问题。此时,需要对共享数据进行适当的同步以防止数据不一致。
- 原子性问题:如果一段代码需要执行一系列的操作,我们希望这些操作作为一个整体,不受其他线程的干扰。然而,在多线程环境下,这些操作可能被其他线程中断,导致结果错误。
- 可见性和顺序性问题:一个线程对共享变量的修改可能无法及时被其他线程看到,或者多个线程之间的操作顺序发生混乱,导致出现线程安全问题。
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线
程安全的。
解决线程安全问题的方法主要有以下几种:
1:使用synchronized关键字
synchronized
会起到互斥效果
,
某个线程执行到某个对象的
synchronized
中时
,
其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
使用synchronized关键字有两种方式:
-
同步方法:在方法前面加上synchronized关键字,表示该方法是一个同步方法,同一时间只有一个线程可以访问该方法。例如:
public synchronized void synchronizedMethod() { // 访问共享资源 }
-
同步代码块:用synchronized关键字加上一个锁对象,表示该代码块是一个同步代码块,同一时间只有一个线程可以访问该代码块。例如:
public void someMethod() { synchronized (lockObject) { // 访问共享资源 } }
- synchronized关键字是针对对象进行同步的,不同的对象需要使用不同的锁对象。
- 尽量减少同步代码块中的操作,以减少锁的持有时间,这样可以减少其他线程等待锁的时间,提高程序的并发性能。
- 避免在同步代码块中调用其他同步方法或同步代码块,否则可能会导致死锁。
- 避免使用可变的锁对象,否则可能会导致多个线程持有相同的锁对象,从而无法实现同步
synchronized 的工作过程 :1. 获得互斥锁2. 从主内存拷贝变量的最新副本到工作的内存3. 执行代码4. 将更改后的共享变量的值刷新到主内存5. 释放互斥锁所以 synchronized 也能保证内存可见性 .
2.使用线程安全的数据结构
Java库中提供了一些线程安全的数据结构,如ConcurrentHashMap、StringBuffer 等,可以方便地在多线程环境下进行操作。这些数据结构内部已经实现了必要的同步机制,可以在多线程环境下保证数据的安全性和一致性。例如:
public class ThreadSafeExample {
public static void main(String[] args) {
// 创建一个StringBuffer对象
StringBuffer stringBuffer = new StringBuffer();
// 创建一个Runnable任务,用于向StringBuffer添加文本
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
synchronized (stringBuffer) {
stringBuffer.append("Thread " + Thread.currentThread().getId() + " was here.\n");
}
}
};
// 创建两个线程并启动它们
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
// 等待两个线程完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印StringBuffer的内容
System.out.println(stringBuffer.toString());
}
}
在这个例子中,我们创建了一个Runnable任务,该任务在两个线程中运行。这个任务向StringBuffer对象添加一行文本。由于StringBuffer是线程安全的,我们不需要担心同时访问和修改它的问题。我们使用synchronized关键字来确保在同一时间只有一个线程可以修改StringBuffer。最后,我们打印出StringBuffer的内容,可以看到两个线程都成功地向它添加了文本。StringBuffer 的核心方法都带有 synchronized
3.volatile 关键字 (保证内存可见性)
当一个变量被声明为volatile 时,它可以确保以下几点:
- 可见性:当一个线程修改了一个
volatile
变量的值,其他线程会立即看到这个变动。这是因为volatile
关键字会禁止 CPU 缓存和编译器优化,从而确保每次读取变量时都会直接从主内存中获取最新值,而不是从本地缓存中读取。这样可以确保在多线程环境下变量值的实时同步。 - 禁止指令重排序:Java 内存模型允许编译器和处理器对指令进行重排序,以提高执行效率。但是,在某些情况下,这种重排序可能导致线程安全问题。
volatile
关键字可以防止这种情况发生,确保指令执行的顺序符合程序员的预期。
但volatile
和
synchronized
有着本质的区别
. synchronized
能够保证原子性
, volatile 保证的是内存可见性.所以volatile 关键字可以确保可见性和禁止指令重排序,但它不能替代synchronize或其他同步机制。在复杂的状态变量或复合操作(例如加法或减法)中,volatile无法保证原子性.