Skip to content

线程安全策略

在Java中,实现线程安全的策略有很多种,以下是一些常见的方法:

1、同步(Synchronization)

使用synchronized关键字来保护临界区代码块或整个方法。当一个线程进入synchronized代码块时,其他线程必须等待该线程执行完后才能获取锁并进入。

2、Lock接口与其实现类

java.util.concurrent.locks.Lock 接口提供了比synchronized更灵活的锁定机制,如可中断、公平锁、读写锁等。具体实现有ReentrantLock、ReentrantReadWriteLock等。

3、volatile关键字

用于确保多线程环境下变量的可见性,但并不能保证原子性。它能确保对 volatile 变量的修改会立即反映到所有线程中,防止了指令重排序问题。

4、原子类(Atomic classes)

java.util.concurrent.atomic 包提供了一系列原子操作类,如AtomicInteger、AtomicLong、AtomicBoolean等,它们内部使用CAS(Compare and Swap)算法来实现无锁化的并发更新。

5、不可变对象(Immutable Objects)

不可变对象一旦创建其状态就不会改变,因此是线程安全的。例如String和包装类Integer(valueOf缓存部分整数)就是典型的不可变对象。

6、ThreadLocal变量

ThreadLocal为每个线程都维护了一个独立的副本,不同线程间互不影响,常用于解决需要在多个方法间传递数据而又不想通过参数的方式传递的情况。

7、设计模式

单例模式中的双重检查锁定(Double-Checked Locking)、生产者消费者模式(Producer-Consumer Pattern)中使用的BlockingQueue等都是为了实现线程安全而采用的设计模式。

8、容器类的线程安全版本

Java集合框架中提供了很多线程安全的容器类,如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等。

9、避免共享状态

尽可能减少线程间的共享状态,或者通过函数式编程风格(无副作用)避免状态冲突。

以上这些策略可以单独或组合使用来确保程序在多线程环境下的正确性和一致性。

线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

被守护对象:被守护对象只能通过获取特定的锁来访问

不可变对象

不可变对象需要满足的条件

1、对象创建以后其状态就不能修改

2、对象所有域都是final类型

3、对象是正确创建的(在对象创建期间,this引用没有逸出)

final关键字

类、方法、变量

修饰类:不能被继承

修饰方法:锁定方法不被继承类修改;效率

修饰变量:基本数据类型变量、引用类型变量

String类的不可变性

通过将字符串存储在常量池中,并限制 String 对象的内容不能被修改来实现的,这种设计提高了字符串对象的安全性、线程安全性和性能优化,并且能够更好地支持字符串的共享和重用

java
private final char value[];

主要通过以下几个原理来实现的:

  1. 字符串存储在常量池中: Java 中的字符串常量(如 "Hello")是存储在特殊的内存区域,称为常量池(String Pool)中的。当创建字符串常量时,JVM 会首先检查常量池中是否已经存在相同内容的字符串,如果存在则返回该字符串的引用,否则在常量池中创建一个新的字符串,并返回其引用。

  2. String 对象的不可变性: String 类中的字符数组 value 是用 final 修饰的,这意味着一旦创建了 String 对象,value 数组的引用就不能被修改。而且 String 类中的所有修改字符串内容的方法(如 substring()concat() 等)都是返回一个新的 String 对象,而不是修改原有的 String 对象。

  3. 安全性和线程安全性: String 类的不可变性提高了字符串对象的安全性和线程安全性。由于字符串是不可变的,所以它们可以被多个线程安全地共享,而不需要额外的同步操作。这样就避免了在并发环境中可能出现的竞态条件和数据不一致性问题。

  4. 性能优化: 由于字符串是不可变的,所以可以缓存字符串的哈希码(hash code),从而提高字符串的哈希算法性能。而且可以在编译时对字符串进行优化,例如字符串的连接操作可以被编译器优化为使用 StringBuilderStringBuffer,从而避免了不必要的字符串对象创建和拼接操作。

Collections.unmodifiableXXX

支持下图的数据结构

image-20240213114147653

示例

java
private static Map<Integer, Integer> map = Maps.newHashMap();

static {
    map.put(1, 2);
    map = Collections.unmodifiableMap(map);
}

public static void main(String[] args) {
    map.put(1, 3);
    log.info("{}", map.get(1));
}

执行报错

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.Collections$UnmodifiableMap.put(Collections.java:1459)
	at cn.diyai.mul_thread.immutable.ImmutableExample2.main(ImmutableExample2.java:22)

Collections.unmodifiableMap方法返回的是一个不可修改的Map对象,对返回的Map对象的任何修改操作都会抛出UnsupportedOperationException异常。

内部实现原理

基于UnmodifiableMap类实现,如果有修改操作,直接抛出UnsupportedOperationException异常

java
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
    return new UnmodifiableMap<>(m);
}


private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
 
    public V put(K key, V value) {
            throw new UnsupportedOperationException();
    }
    ...
}

Guava的ImmutableXX

示例

java
private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);

private final static ImmutableSet set = ImmutableSet.copyOf(list);

private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4);

private final static ImmutableMap<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder()
            .put(1, 2).put(3, 4).put(5, 6).build();

添加依赖

xml
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

线程封闭

线程封闭(Thread Confinement)是一种多线程编程的技术,用于确保数据在多线程环境中的安全访问。

通过将数据限制在特定的线程范围内,使得数据只能被特定的线程访问,从而避免了多线程环境下的竞态条件和数据不一致性问题。

线程封闭通常可以通过以下几种方式实现:

1、堆栈封闭(Stack Confinement):

将数据保存在线程的堆栈(stack)中,确保数据只能在当前线程的方法调用链中被访问。这种方式适用于数据的生命周期与线程的方法调用链相匹配的情况,通常不需要额外的同步措施。

java
public class StackConfinementExample {
    public void process() {
        int data = 10; // 数据保存在方法中的局部变量中
        // 在方法中使用数据
        System.out.println("Data processed: " + data);
    }
}

2、栈封闭(Stack Confinement)

将数据保存在局部变量或者方法参数中,确保数据只能在当前线程的方法执行过程中被访问。这种方式适用于方法内部的局部变量或者方法参数,并且通常不需要额外的同步措施。

java
public class MethodParameterConfinementExample {
    public void process(int data) { // 将数据作为方法的参数传递
        // 在方法中使用数据
        System.out.println("Data processed: " + data);
    }
}

3、ThreadLocal 封闭

使用 ThreadLocal 类来保存线程本地变量,确保数据只能被特定线程访问。ThreadLocal 提供了 get()set() 方法来获取和设置线程本地变量,每个线程都有自己的一份副本,互相之间不会干扰。

java
public class ThreadLocalConfinementExample {
    private static final ThreadLocal<Integer> threadLocalData = ThreadLocal.withInitial(() -> 20);

    public void process() {
        int data = threadLocalData.get(); // 从 ThreadLocal 中获取线程本地变量
        // 在方法中使用数据
        System.out.println("Data processed: " + data);
    }
}

4、线程池封闭

在使用线程池执行任务时,通过将数据作为任务的参数传递给线程池,确保数据只能在特定的线程池中被访问。这种方式适用于需要在多个线程之间共享数据的情况,并且可以通过线程池的生命周期来控制数据的访问范围。

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolConfinementExample {
    private static final ExecutorService threadPool = Executors.newFixedThreadPool(5);

    public void processData(int data) {
        // 将数据作为任务的参数传递给线程池执行
        threadPool.execute(() -> {
            // 在任务中使用数据
            System.out.println("Data processed: " + data);
        });
    }

    // 在程序结束时关闭线程池
    public void shutdownThreadPool() {
        threadPool.shutdown();
    }
}

线程不安全类及优化方法

并发模拟代码

StringBuild

java
@Slf4j
public class StringExample1 implements Runnable{
    public static StringBuilder stringBuilder = new StringBuilder();
    private void append() {
        stringBuilder.append("0");
        // 这里可以添加具体的业务操作
    }

    @Override
    public void run() {
        append();
    }

    public static void main(String[] args) throws InterruptedException {
        StringExample1 demo = new StringExample1();
        ConcurrentExecutor<Void> executor = new ConcurrentExecutor<>(5000, 200, demo);
        executor.execute();
        log.info("size: {}", stringBuilder.toString().length()); // size: 4956
    }
}

推荐:StringBuffer线程安全

java
@Slf4j
public class StringExample3 implements Runnable{
    public StringBuffer stringBuilder = new StringBuffer();
    private void append() {
        stringBuilder.append("0");
        // 这里可以添加具体的业务操作
    }

    @Override
    public void run() {
        append();
    }

    public static void main(String[] args) throws InterruptedException {
        StringExample3 demo = new StringExample3();
        ConcurrentExecutor<Void> executor = new ConcurrentExecutor<>(5000, 200, demo);
        executor.execute();
        log.info("size: {}", stringBuilder.toString().length()); // size: 5000
    }
}

SimpleDateFormat

java
public class DateFormatExample1 implements Runnable {

    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
    private void update() {
        try {
            simpleDateFormat.parse("20240210");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }

    @Override
    public void run() {
        update();
    }

    public static void main(String[] args) throws Exception {
        DateFormatExample1 demo = new DateFormatExample1();
        ConcurrentExecutor<Void> executor = new ConcurrentExecutor<>(5000, 200, demo);
        executor.execute();
    }
}

执行报错

xml
rmatExample1 - parse exception
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at cn.diyai.mul_thread.commonUnsafe.DateFormatExample1.update(DateFormatExample1.java:27)
	at cn.diyai.mul_thread.commonUnsafe.DateFormatExample1.run(DateFormatExample1.java:35)
	at cn.diyai.mul_thread.ConcurrentExecutor.lambda$execute$0(ConcurrentExecutor.java:39)

修改办法

java
private void update() {
        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            simpleDateFormat.parse("20240210");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }

基于堆栈封闭

推荐 JodaTime

java
public class DateFormatExample5 implements Runnable {
    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
    private void update() {
        log.info("{}", DateTime.parse("20240210", dateTimeFormatter).toDate());
    }

    @Override
    public void run() {
        update();
    }

    public static void main(String[] args) throws Exception {
        DateFormatExample5 demo = new DateFormatExample5();
        ConcurrentExecutor<Void> executor = new ConcurrentExecutor<>(5000, 200, demo);
        executor.execute();
    }
}

pom需添加依赖

xml
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.4</version>
</dependency>

ArrayList

java
public class ArrayListExample1 implements Runnable{
    @Getter
    private List<Integer> list = new ArrayList<>();

    private void update() {
        list.add(1);
    }

    @Override
    public void run() {
        this.update();
    }


    public static void main(String[] args) throws Exception {
        ArrayListExample1 demo = new ArrayListExample1();
        ConcurrentExecutor<Void> executor = new ConcurrentExecutor<>(5000, 200, demo);
        executor.execute();
        log.info("list size {}",demo.getList().size()); // list size 4966
    }
}

线程安全方法

1、使用Vector

java
List<Integer> list = new Vector<>();

2、使用同步容器synchronizedList

java
List<Integer> list = Collections.synchronizedList(Lists.newArrayList());

3、使用并发容器CopyOnWriteArrayList

java
List<Integer> list = new CopyOnWriteArrayList<>();

HashSet

java
public class HashSetExample {

    // 请求总数
    public static int requestTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Set<Integer> set = new HashSet<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        for (int i = 0; i < requestTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", set.size()); // size:4844
    }

    private static void update(int i) {
        set.add(i);
    }
}

线程安全方法

1、使用synchronizedSet

java
Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());

2、使用ConcurrentSkipListSet

java
Set<Integer> set = new ConcurrentSkipListSet<>();

3、使用CopyOnWriteArraySet

java
Set<Integer> set = new CopyOnWriteArraySet<>();

HashMap

java
public class HashMapExample {

    // 请求总数
    public static int requestTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        for (int i = 0; i < requestTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}

线程安全方法

1、使用Hashtable

java
private static Map<Integer, Integer> map = new Hashtable<>();

2、使用synchronizedMap

java
Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());

3、使用并发容器ConcurrentSkipListMap

java
Map<Integer, Integer> map = new ConcurrentSkipListMap<>();

4、使用ConcurrentHashMap

java
Map<Integer, Integer> map = new ConcurrentHashMap<>();