【Java第75集】java实现线程同步的方式详解

文章目录

  • 一、无状态代码
  • 二、不可变对象
  • 三、`synchronized` 关键字
  • 四、Lock接口
  • 五、分布式锁
  • 六、`volatile` 关键字
  • 七、`ThreadLocal`对象
  • 八、JUC线程安全集合类
    • 1. 基于锁的集合类
    • 2. 基于 CAS 和分段锁的集合类
    • 3. 基于写时复制的集合类
    • 4. 阻塞队列(Blocking Queue)
  • 九、CAS原子类
  • 十、数据隔离设计

线程安全问题是我们每个Java后端开发必知必会的知识点,今天我们就一文搞定如何实现线程同步,细讲目前常见的实现线程同步的方式。

一、无状态代码

当多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们的整个类的代码没有使用公共资源,那么这个类就是线程安全的。比如:

public class StatelessService {

	public void add(String status) {
		System.out.println("add status:" + status);
	}
	
	public void update(String status) {
		System.out.println("update status:" + status);
	}
}

二、不可变对象

在 Java 中,不可变对象(Immutable Object)是指其内部状态在创建后无法被修改的对象。这种特性带来了线程安全、易于维护和共享等优势。

以下是 Java 中常见的不可变对象及其分类:

  • 基础类型不可变对象String
  • 基本类型包装类IntegerLongFloatDoubleShortByteCharacterBoolean
  • 数值处理类BigIntegerBigDecimal
  • 不可变日期时间类LocalDateLocalTimeLocalDateTimeZonedDateTime
  • 枚举类型(Enum):枚举实例在创建后不可变
  • 不可变集合:通过 Collections.unmodifiableXXX() 方法创建,返回的集合视图不可修改,尝试修改会抛出 UnsupportedOperationException
  • Java 9+ 不可变集合:通过List.of(T... elements)Set.of(T... elements)Map.of(K, V, K, V...)创建
  • 自定义不可变类:自行实现不可变对象类,主要方法如下。
    • 将类声明为 final,防止继承。
    • 所有字段声明为 private final
    • 不提供 setter 方法。
    • 如果包含可变对象(如 List),需在构造器中进行防御性复制。

三、synchronized 关键字

synchronized 是 Java 中用于实现线程同步的关键字,其核心目标是确保多线程环境下共享资源的安全访问,避免因并发操作导致的数据不一致或竞态条件问题。

synchronized 可以通过以下三种方式实现线程同步:

  • 同步实例方法

    • 锁对象:当前实例对象(this)。
    • 特点:同一实例的同步方法在同一时刻只能被一个线程访问。
    • 示例
      public class Counter {
          private int count = 0;
          public synchronized void increment() {
              count++; // 线程安全的操作
          }
      }
      
  • 同步静态方法

    • 锁对象:当前类的 Class 对象(如 Counter.class)。
    • 特点:所有实例的同步静态方法在同一时刻只能被一个线程访问。
    • 示例
      public class Counter {
          private static int staticCount = 0;
          public static synchronized void incrementStatic() {
              staticCount++; // 线程安全的操作
          }
      }
      
  • 同步代码块

    • 锁对象:可灵活指定对象(如 lock)。
    • 特点:仅对代码块内的代码加锁,缩小同步范围,提高并发性能。
    • 示例
      public class Counter {
          private int count = 0;
          private final Object lock = new Object(); // 专用锁对象
          public void increment() {
              synchronized (lock) {
                  count++; // 仅对 count++ 操作加锁
              }
          }
      }
      

四、Lock接口

Lock 接口是 Java 并发包(java.util.concurrent.locks)中提供的一个线程同步机制,用于替代传统的 synchronized 关键字。它提供了比 synchronized 更灵活、更强大的同步控制能力,尤其适用于复杂并发场景的需求。

ReentrantLockLock 接口最常用的实现类,支持可重入性(同一线程可多次获取同一把锁)和公平锁/非公平锁的配置。以下是ReentrantLock的代码示例:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁(必须在finally中确保释放)
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount()); // 输出 2000
    }
}

五、分布式锁

如果是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。但如果在分布式集群环境中,即某个应用如果部署了多个集群下的多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全。

此时就需要考虑使用分布式锁了。分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。

这里我们以比较常用的redis分布式锁举例,使用 Jedis 实现 Redis 分布式锁,详细说明如下:

  • 核心原理

    • 加锁:使用 SET key value NX EX 命令,确保原子性设置锁和过期时间。
    • 解锁:使用 Lua 脚本验证持有者并删除锁,避免误删其他客户端的锁。
  • 代码示例

    import redis.clients.jedis.Jedis;
    import java.util.Collections;
    
    public class RedisDistributedLock {
        // Redis 连接实例
        private Jedis jedis;
        // 锁的键名
        private String lockKey;
        // 客户端唯一标识(用于释放锁)
        private String clientId;
        // 锁的过期时间(秒)
        private int expireTime;
    
        public RedisDistributedLock(Jedis jedis, String lockKey, String clientId, int expireTime) {
            this.jedis = jedis;
            this.lockKey = lockKey;
            this.clientId = clientId;
            this.expireTime = expireTime;
        }
    
        /**
         * 尝试获取锁
         * @return 是否成功获取锁
         */
        public boolean tryLock() {
            String result = jedis.set(lockKey, clientId, "NX", "EX", expireTime);
            return "OK".equals(result);
        }
    
        /**
         * 释放锁
         */
        public void unlock() {
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                               "return redis.call('del', KEYS[1]) " +
                               "else return 0 end";
            jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
        }
    
        // 示例:使用锁
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost", 6379);
            String lockKey = "resource_lock";
            String clientId = java.util.UUID.randomUUID().toString();
            RedisDistributedLock lock = new RedisDistributedLock(jedis, lockKey, clientId, 30); // 锁过期时间为30秒
    
            if (lock.tryLock()) {
                try {
                    // 执行业务逻辑
                    System.out.println("Lock acquired, executing critical section...");
                    Thread.sleep(10000); // 模拟业务操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock(); // 释放锁
                    System.out.println("Lock released.");
                }
            } else {
                System.out.println("Failed to acquire lock.");
            }
    
            jedis.close();
        }
    }
    
  • 关键点说明

    • 唯一标识clientId 使用 UUID 或线程 ID 作为唯一标识,确保只有持有锁的客户端才能释放。
    • 原子性:通过 Lua 脚本保证 GETDEL 操作的原子性。
    • 过期时间:避免因客户端异常退出导致死锁。
    • 异常处理:在 finally 块中释放锁,确保资源释放。

六、volatile 关键字

当同一时间内有且只有一个线程对数据修改,且要保证其他线程全部读取到最新的修改后的数据状态,在这种场景下我们就可以使用volatile 关键字来保证数据的可见性。

public class VolatileExample {
    private volatile boolean flag = true; // 确保可见性

    public void run() {
        while (flag) {
            // 循环直到 flag 被修改为 false
        }
        System.out.println("Thread stopped.");
    }

    public void stop() {
        flag = false; // 修改后,其他线程立即感知
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        Thread thread = new Thread(() -> example.run());
        thread.start();

        Thread.sleep(1000); // 主线程等待1秒
        example.stop(); // 修改 flag
        thread.join();
    }
}

七、ThreadLocal对象

ThreadLocal(线程局部变量)是 Java 提供的一种线程安全机制,通过为每个线程维护独立的变量副本,确保每个线程只能访问自己的副本,避免线程间的数据竞争。

ThreadLocal提供类get()和set()等方法,为每个使用该变量的线程都保存一份独立的副本。Thread类中有一个成员变量ThreadLocal.ThreadLocalMap,ThreadLocalMap则定义在ThreadLocal类中的静态内部类,它是一个Map,他的key是线程对象,value是线程的变量副本。

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocal.get();
            threadLocal.set(value + 1);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
    }
}

八、JUC线程安全集合类

在 Java 多线程编程中,线程安全的集合类(java.util.concurrent 包)是解决并发问题的核心工具。与传统的 VectorHashtableCollections.synchronizedXXX 不同,JUC 提供的集合类在性能和并发能力上进行了深度优化,适用于高并发场景。以下是对 JUC 线程安全集合类的详细介绍:

1. 基于锁的集合类

  • 实现方式:通过 synchronizedReentrantLock 锁控制并发访问。
  • 典型类
    • Vector
    • Hashtable
    • Stack
    • Collections.synchronizedList(new ArrayList<>())
    • Collections.synchronizedMap(new HashMap<>())
  • 特点
    • 简单直接,但性能较低(全表锁)。
    • 适用于低并发或简单场景。

2. 基于 CAS 和分段锁的集合类

  • 实现方式:使用 CAS(Compare and Swap)和分段锁(Segment)减少锁粒度。
  • 典型类
    • ConcurrentHashMap
    • ConcurrentSkipListMap
    • ConcurrentSkipListSet
  • 特点
    • 高性能,支持高并发。
    • 适用于读写频繁的场景。

3. 基于写时复制的集合类

  • 实现方式:写操作时复制整个集合,读操作无锁。
  • 典型类
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
  • 特点
    • 读操作性能极高,适合读多写少的场景。
    • 写操作开销大,占用额外内存。

4. 阻塞队列(Blocking Queue)

  • 实现方式:基于锁和条件队列实现阻塞操作。
  • 典型类
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • PriorityBlockingQueue
    • SynchronousQueue
    • DelayQueue
  • 特点
    • 支持线程间协作(生产者-消费者模型)。
    • 适用于任务队列、缓存等场景。

九、CAS原子类

JUC 提供了基于 CAS(Compare and Swap)的原子类,用于实现无锁的线程安全操作。常见原子类如下:

  • AtomicIntegerAtomicLong:原子整数操作。
  • AtomicReference:原子引用类型操作。
  • AtomicBoolean:原子布尔操作。
  • AtomicArray:原子数组操作。
  • 示例
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicCounter {
        private AtomicInteger count = new AtomicInteger(0);
    
        public void increment() {
            count.incrementAndGet(); // 原子自增
        }
    
        public int getCount() {
            return count.get();
        }
    
        public static void main(String[] args) throws InterruptedException {
            AtomicCounter counter = new AtomicCounter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    counter.increment();
                }
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    counter.increment();
                }
            });
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            System.out.println("Final count: " + counter.getCount()); // 输出 2000
        }
    }
    

十、数据隔离设计

有时候,我们在操作集合数据时,可以通过数据隔离设计 ,来保证线程安全。

比如每个用户只被线程池中的一个线程处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。

public class ThreadPoolExample {

    public static void main(String[] args) {

        ExecutorService threadPool = new ThreadPoolExecutor(8,
                10,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue(500),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<User> userList = Lists.newArrayList(
                new User(1L, "汤姆", 11, "北京"),
                new User(2L, "李雷", 13, "天津"),
                new User(3L, "韩梅梅", 18, "云南"));

        for (User user : userList) {
            threadPool.submit(new Work(user));
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(userList);
    }

    static class Work implements Runnable {
        private User user;

        public Work(User user) {
            this.user = user;
        }

        @Override
        public void run() {
            user.setName(user.getName() + "测试");
        }
    }
}

数据隔离设计还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。

这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。

你可能感兴趣的:(【Java第75集】java实现线程同步的方式详解)