Day31 线程安全一
一、概念
线程安全是指在多线程环境下,对共享数据的操作不会导致数据出现不一致或不确定的情况,保证多个线程同时访问共享资源时不会产生竞态条件(Race Condition)或其他并发问题。
重要性: 确保线程安全是编写并发程序时必顫考虑的重要问题
二、实现方法
- 加锁机制:使用 synchronized 关键字或 Lock 接口来对共享资源进行加锁,确保同一时间只有一个线程可以访问共享资源,避免数据竞争。
- 使用并发容器:Java 提供了一系列线程安全的并发容器,如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些容器内部实现了线程安全机制,可以避免多线程并发访问时的问题。
- 使用原子类:Java 提供了一系列原子类,如 AtomicInteger、AtomicLong 等,它们提供了一些原子性操作,能够保证多线程环境下的安全访问。
- 使用线程局部变量:ThreadLocal 可以实现线程范围内的共享变量,每个线程都有自己独立的变量副本,避免了多线程访问共享变量的竞争。
- 避免可变状态:尽量避免共享可变状态,使用不可变对象或线程安全的对象来代替可变对象,减少共享资源的修改。
- 使用并发工具类:Java 提供了一些并发工具类,如 CountDownLatch、Semaphore、CyclicBarrier 等,可以帮助管理多线程环境下的并发访问。
三、加锁机制
1、synchronized加锁
1.1. 概念:
synchronized
是 Java 中用于实现线程同步的关键字,可以应用于方法或代码块上,确保多个线程对共享资源的安全访问。使用 synchronized
可以避免多线程环境下的数据竞争和并发问题,确保线程安全。
1.2. 主要特点:
- 方法同步:可以将
synchronized
关键字应用于方法上,确保同一时间只有一个线程可以访问该方法。例如:public synchronized void method() { ... }
。 - 代码块同步:可以将
synchronized
关键字应用于代码块上,指定需要同步的对象或类。例如:
synchronized (obj) {
// 同步代码块
}
- 内置锁:每个 Java 对象都有一个内置锁(Intrinsic Lock 或 Monitor Lock),当使用
synchronized
关键字时,实际上是获取了对象的内置锁。 - 互斥性:
synchronized
关键字保证了同一时间只有一个线程可以获取对象的锁,其他线程需要等待锁释放后才能继续执行。 - 可重入性:同一个线程可以多次获取同一个对象的锁,确保了代码的可重入性。
- 释放锁:当同步代码块执行完毕或发生异常时,锁会自动释放,其他线程可以继续竞争锁。
- 类锁:
synchronized
还可以应用于静态方法或静态代码块上,实现类级别的同步。
理解:
同步代码块:
synchronized(锁对象){//自动上锁
//...想要互斥的代码...
}//自动解锁
同步方法
同步方法 – 成员同步方法:
注意:锁对象 -> this
public synchronized void method(){//自动上锁
//...想要互斥的代码...
}//自动解锁
同步方法 – 静态同步方法:
注意:锁对象 -> 类.class
public static synchronized void method(){//自动上锁
//...想要互斥的代码...
}//自动解锁
2、lock加锁
2.1概念:
Lock
接口是 Java 并发包(java.util.concurrent.locks)中定义的一种锁机制,用于替代传统的 synchronized 关键字来实现线程同步。相比于 synchronized 关键字,Lock
接口提供了更多的灵活性和功能,可以更精细地控制多线程之间的并发访问。Lock
接口的常用实现类是 ReentrantLock
,它是可重入锁,支持公平锁和非公平锁。除了 ReentrantLock
,Java 还提供了其他实现 Lock
接口的类,如 ReentrantReadWriteLock.ReadLock
和 ReentrantReadWriteLock.WriteLock
。
2.2 lock定义的方法:
void lock()
:获取锁,如果锁不可用,则当前线程将一直等待。void unlock()
:释放锁。boolean tryLock()
:尝试获取锁,如果锁可用,则立即返回 true;否则返回 false。void lockInterruptibly()
:获取锁,但允许响应中断。Condition newCondition()
:返回一个与该锁相关的条件对象,用于实现等待/通知机制。
2.3 示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}
理解:
//锁对象
Lock lock = new ReentrantLock();
lock.lock();//手动上锁
//...想要互斥的代码...
lock.unlock();//手动解锁
四、线程安全 – 单例模式(懒汉式)
1、概念:
在懒汉式单例模式中,单例实例在第一次被调用时才会被实例化,以延迟加载的方式创建单例对象。在多线程环境下,懒汉式单例模式需要考虑线程安全性,以确保在并发情况下仍能保持单例的唯一性。
2、示例:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
- 将
instance
声明为volatile
,确保多线程之间的可见性,避免指令重排序问题。 - 在
getInstance()
方法中使用双重检查锁定(Double-Checked Locking)机制,确保只有第一次调用时才会创建实例。 - 在同步块内部再次检查
instance
是否为null
,避免多个线程同时通过第一次判空检查,导致多次创建实例。
3、理解:
该类的对象在整个项目中只创建一次(只实例化一次)
注意:单例模式(懒汉式)不是线程安全的
缺点:如果只调用了类里的静态方法,没用到单例对象,就是浪费空间
五、线程安全 – 枚举单例模式(饿汉式)
1、概念:
枚举单例模式是一种线程安全的单例模式实现方式,也是一种饿汉式单例模式的变种。在枚举单例模式中,单例实例是通过枚举类来实现的,枚举类保证了在任何情况下都只会有一个实例存在,且线程安全。
2、示例:
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// 单例方法
}
}
代码理解: 在枚举单例模式中,EnumSingleton.INSTANCE
就是单例实例,它在类加载时被初始化,保证了线程安全性。枚举类的特性确保了在任何情况下都只会有一个实例存在,不受多线程环境的影响。
3、优点:
- 线程安全:枚举类的实例在类加载时被初始化,保证了线程安全性。
- 防止反射攻击:枚举类不允许通过反射来创建实例,避免了反射攻击。
- 防止序列化问题:枚举类默认实现了序列化机制,可以防止通过序列化和反序列化来破坏单例模式。
4、理解:
理解:该类的对象在整个项目中只创建一次(只实例化一次)
注意:枚举单例模式(饿汉式)是线程安全的
缺点:如果只调用了枚举里的静态方法,没用到单例对象,就是浪费空间
六、线程安全 – 双重检验锁的单例模式
1、概念:
双重检验锁(Double-Checked Locking)是一种常用的单例模式实现方式,通过在实例化代码块内部进行双重检查来确保在多线程环境下仅创建一个实例。
2、示例:
public class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {
// 私有构造方法
}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
代码关键点:
- 将
instance
声明为volatile
,确保多线程之间的可见性,避免指令重排序问题。 - 在
getInstance()
方法中使用双重检查锁定机制,第一次检查instance
是否为null
,第二次在同步块内部再次检查instance
是否为null
,确保只有第一次调用时才会创建实例。 - 双重检查锁定可以减少同步开销,提高性能,同时保证了线程安全性。
3、重要性:
双重检验锁的单例模式在多线程环境下能够确保线程安全,避免了多线程并发访问时可能出现的问题。需要注意的是,在 Java 5 及以上版本,建议使用 volatile
关键字来确保线程安全性。
4、理解:
理解:该类的对象在整个项目中只创建一次(只实例化一次)
注意:双重检验锁的单例模式是线程安全的