Java 多线程之同步(锁)相关类总结

2023-12-15 02:44:04

一、概述

  • 要实现线程同步,原理就是加锁。只是看什么情况使用什么锁,以及锁的粒度问题。

二、volatile 可见性/有序性

  • 详细使用说明请看Java 多线程之 volatile

  • 提供可见性,保证有序性。虽然提供了可见性和有序性的保证,但它并不能保证原子性。对于复合操作,如递增或递减操作,仍然需要使用其他机制,如锁或原子类来保证原子性。

  • 使用方法

    volatile int count1 = 1;
    private volatile int count2 = 2;
    volatile boolean flag1 = false;
    private volatile boolean flag2 = false;
    

三、synchronized 互拆锁/排他锁/非观锁

  • 详细使用说明请看Java 多线程之 synchronized

  • synchronized 提供了一种简单而强大的机制来控制多个线程之间的并发访问,确保共享资源的安全性和一致性。它解决了多线程环境中的竞态条件、数据竞争和内存模型等问题,是实现线程安全的重要手段之一。

  • 作用在代码上,相当于给代码块加锁(Lock)

      	public void performTask() {
            // synchronized 作用于代码块
        	synchronized (lock) {
          		// 业务逻辑,同步代码块,对共享资源进行操作
        	}
      	}
    
  • 作用在方法上,相当于给整个方法加锁(Lock)

        // synchronized 作用在方法上
      	public synchronized void increment() {
            // 业务逻辑,同步代码块,对共享资源进行操作
      	}
    

四、DCL(Double-Checked Locking)

  • 详细使用说明请看Java 多线程之 DCL

  • DCL 是 Double-Checked Locking 的缩写,是一种用于在多线程环境下延迟初始化对象的技术。它的目标是在保持高性能的同时,确保只有一个线程执行对象的初始化过程。

  • DCL 的实现通常基于以下步骤:

    1. 检查对象是否已经被创建。如果已经创建,则直接返回对象。
    2. 如果对象尚未创建,则尝试获取锁。
    3. 获取锁后,再次检查对象是否已经被创建(在获取锁之前的检查只是为了避免不必要的同步)。
    4. 如果对象尚未创建,则进行对象的创建和初始化。
    5. 释放锁。
    6. 返回对象。
  • 使用方法如下,Singleton 是一个在高并发下,多线要使用的延迟初始化单例类

    public class Singleton {
    	private static volatile Singleton instance;
      	private Singleton() {
        	// 私有构造函数
      	}
    
      	public static Singleton getInstance() {
     		if (instance == null) { // 1.检查对象是否已经被创建
        		synchronized (Singleton.class) { // 2.尝试获取锁
            		if (instance == null) { // 3.再次检查对象是否已经被创建
            			instance = new Singleton();// 4.如果对象尚未创建,则进行对象的创建和初始化
            		}
         		}// 5.释放锁(synchronized 语句块结束自动释放锁)
        	}
        	return instance; // 6.返回对象
      	}
    }
    

    需要注意的是 Singleton instance 对象的定义需要使用 volatile 关键字。

五、CAS(Compare and Set)

  • 详细使用说明请看Java 多线程之 CAS

  • 实现无锁优化,是自旋锁/乐观锁的实现方式。

  • CAS 是 Compare and Set(比较并设置)的缩写,是一种并发算法,用于实现多线程环境下的原子操作。

  • CAS 操作涉及三个操作数:内存位置(或称为变量的值)、期望值和新值。它的执行过程是:将内存位置的当前值与期望值进行比较。如果相等,则将新值写入内存位置;如果不相等,则说明其他线程已经修改了内存位置的值,操作失败。

  • CAS只能检测到预期值是否相等,无法感知到变量值的修改过程中是否发生了其他的并发修改,可能会引发ABA问题。

  • java.util.concurrent.atomic.Atomic* 开头的类都用 CAS 实现无锁优化,因此在多线程环境中能用这些类就尽量不用悲观锁相关类。

            AtomicBoolean atomicBoolean = new AtomicBoolean();
            AtomicInteger atomicInteger = new AtomicInteger();
            AtomicLong atomicLong = new AtomicLong();
            AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
            AtomicLongArray atomicLongArray = new AtomicLongArray(new long[5]);
            //AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = new AtomicIntegerFieldUpdater();
    		// 等等 ...
    

六、ReentrantLock 可重入锁/公平/非公平锁

  • 详细使用说明请看Java 多线程之 ReentrantLock

  • ReentrantLock 是一个可重入锁,与 synchronized 关键字相比,ReentrantLock 提供了更灵活、更强大的功能,同时也更复杂。

  • 公平锁/非公平锁

    • 直接使用 ReentrantLock 的 lock 和 unlcok 方法,基本功能同 synchronized 关键字。但是他比 synchronized 强大的地方在于他可以设置为公平锁和非公平锁,以及使用可中断获取锁和超时获取锁方法。

      class XXXXXX {
          private ReentrantLock lock = new ReentrantLock(true); // 公平锁
          //private ReentrantLock lock = new ReentrantLock(false); // 非公平锁
          public void increment() {
              lock.lock(); // 获取锁
              // lock.lockInterruptibly(); // 获取可中断的锁
              // lock.tryLock(3000, TimeUnit.SECONDS);// 在指时间内获取锁
              try {
                  // 业务逻辑
              } finally {
                  lock.unlock();// 释放锁
              }
          }
      }
      
  • 条件变量 (Condition)

    • ReentrantLock 的条件变量(Condition)的使用,实现线程等待/通知机制。

      class BoundedQueue<T> {
          private Queue<T> queue = new LinkedList<>();
          private int capacity;
          private ReentrantLock lock = new ReentrantLock();
          private Condition notFull = lock.newCondition();
          private Condition notEmpty = lock.newCondition();
      
          public BoundedQueue(int capacity) {
              this.capacity = capacity;
          }
      
          public void enqueue(T item) throws InterruptedException {
              lock.lock();
              try {
                  while (queue.size() == capacity) {
                      // 条件1达到, 业务逻辑1等待
                      notFull.await();
                  }
                  queue.add(item);
                  // 通知业务逻辑2执行
                  notEmpty.signalAll();
              } finally {
                  lock.unlock();
              }
          }
      
          public T dequeue() throws InterruptedException {
              lock.lock();
              try {
                  while (queue.isEmpty()) {
                      // 条件2达到,业务逻辑2等等
                      notEmpty.await();
                  }
                  T item = queue.poll();
                  // 通知业务逻辑1执行
                  notFull.signalAll();
                  return item;
              } finally {
                  lock.unlock();
              }
          }
      }
      

七、ReentrantReadWriteLock 读写锁/共享锁/排他锁

  • 详细使用说明请看Java 多线程之 ReentrantReadWriteLock

  • ReentrantReadWriteLock 是Java中提供的一种读写锁实现,它允许多个线程同时读取共享资源,但在写操作时需要独占访问。它是对传统互斥锁的一种改进,可以提高并发性能。

  • 读写锁的主要目的是在读多写少的场景下,提供更高的并发性能。当多个线程只需读取共享资源时,可以同时获得读锁,从而实现并发读取。而当有线程需要对共享资源进行写操作时,它必须独占地获取写锁,在此期间,其他线程无法获取读锁或写锁,从而确保数据的一致性和完整性。

  • 获取读锁

    rwLock.readLock().lock();
    try {
        // 访问共享资源的读操作
    } finally {
        rwLock.readLock().unlock();
    }
    
  • 获取写锁

    rwLock.writeLock().lock();
    try {
      // 访问共享资源的写操作
    } finally {
      rwLock.writeLock().unlock();
    }
    

八、CountDownLatch 计数等待/同步辅助类

  • 详细使用说明请看Java 多线程之 CountDownLatch

  • CountDownLatch 是Java中提供的一种同步工具类,用于控制多个线程之间的执行顺序和协调。

  • CountDownLatch 通过一个计数器来实现,该计数器初始化为一个正整数,表示需要等待的线程数目。每个线程执行完一定的任务后,会调用countDown()方法将计数器减1。当计数器减到0时,表示所有线程已经完成任务,等待在await()方法处的线程被唤醒,继续执行后续操作。

  • 使用方法

    int threadCount = 3;
    CountDownLatch latch = new CountDownLatch(threadCount);
    
    for (int i = 0; i < threadCount; i++) {
        // 启动一些子线程来执行任务
        Thread thread = new Thread(() -> {
            // 子线程执行任务
            System.out.println("执行业务逻辑");
            // 子线程任务完成,计数器减1
            latch.countDown();
        });
        thread.start();
    }
    
    // 主线程 等待所有线程完成任务
    latch.await();
    // 所有子线程完成任务后 主线程继续执行后续操作
    System.out.println("执行后续业务");
    
    

九、CyclicBarrier 并行任务/数据加载/同步辅助类

  • 详细使用说明请看Java 多线程之 CyclicBarrier

  • CyclicBarrier(循环屏障)是Java并发编程中的一种同步辅助工具。它允许一组线程相互等待,直到所有线程都到达一个共同的屏障点,然后继续执行后续操作。CyclicBarrier可以用于解决多线程任务的协调和同步问题。

  • CyclicBarrier 的主要作用是使多个线程在某个点上进行同步等待所有线程都到达该点后再一起继续执行

    • 它类似于一组线程开始跑步,跑到3000米时停下等待其他线程跑完全,全部到后裁判统计分数,然后再同时开始启跑。
    • 我感觉有点像三峡大坝的船过闸一样的,假如一些船开到三峡大坝的闸内,这里他们不能过闸,但等一定数量的船后,闸门打开,这些船只开始继续航行。
    • 实际应用如下载N个文件碎片,都完成后按顺序合成一个,然后再进行分析。
  • 使用方法

    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class CyclicBarrierExample {
        private static final int THREAD_COUNT = 3;
    
        public static void main(String[] args) {
            CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
                // 执行屏障动作(最后一个到达的线程执行该动作)。
                // 特别说明:能执行到这里说明,规定数量的线程(THREAD_COUNT)都执行了 barrier.await();            
            });
    
            for (int i = 0; i < THREAD_COUNT; i++) {
                Thread thread = new Thread(() -> {
                    try {
                        // 执行业务逻辑
                        
                        barrier.await(); // 线程到达屏障点,等待其他线程
                        
                        // 所以线程都到达屏障点,并且已经执行屏障作用后,才会执行这里的代码
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                });
                thread.start();
            }
        }
    }
    

十、Phaser 多阶段任务/同步辅助类

  • 详细使用说明请看Java 多线程之 Phaser

  • Phaser 也是Java并发编程中的一种同步辅助工具,用于线程之间的协调和同步。它提供了比CyclicBarrier和CountDownLatch更灵活和强大的功能,可以用于更复杂的多线程协作场景。

  • Phaser的主要用途是将多个线程分为多个阶段,并在每个阶段进行同步。每个线程可以独立运行,但在特定的阶段需要等待其他线程到达屏障点。Phaser可以动态地适应线程的注册和注销,可以处理可变数量的参与者。

  • 使用方法

    import java.util.concurrent.Phaser;
    
    public class MultiPhaseTask {
        
        private static final int NUM_THREADS = 3;
    
        public static void main(String[] args) {
    
            // 注册线程可以通过构造函数的参数指定,也可以通过 phaser.register(); 方法注册线程。
            Phaser phaser = new Phaser(NUM_THREADS) {
          		@Override
          		protected boolean onAdvance(int phase, int registeredParties) {
            		System.out.println("第" + phase + "阶段完成");
            		return false; // 返回 true 会终止 phaser
          		}
        	};
    
            for (int i = 0; i < NUM_THREADS; i++) {
                // phaser.register(); // 如果上面造函数的参数没有指定,则启用这句话来注册线程
                Thread thread = new Thread(() -> {
                    
                    // 使用 for 循环来模拟3个阶段
                    for (int phase = 1; phase <= 3; phase++) {
                        // 这是第 phase 阶段
                        System.out.println("执行第 phase 阶段 任务...");               
                        phaser.arriveAndAwaitAdvance(); // 等待其他线程到达屏障点
                    }
                    
                    System.out.println("完成所有阶段");
                    // 解除线程的注册
                    phaser.arriveAndDeregister();
                });
                
                thread.start();
            }
        }
    }
    

十一、StampedLock 读写锁,共享锁/排他锁

  • 详细使用说明请看Java 多线程之 StampedLock

  • StampedLock是Java 8引入的一种读写锁的实现,它提供了一种乐观的读锁(Optimistic Read Lock)和悲观的读锁(Pessimistic Read Lock),和写锁(Write Lock),以及对读-写冲突的解决方案。StampedLock的设计目标是在读多写少的场景下提供更高的并发性能。与传统的读写锁相比,StampedLock更加灵活和高效。

  • 与前面的 ReentrantReadWriteLock 相比,StampedLock 在某些情况下可以提供更高的性能,但并不是在所有情况下都表现更好。StampedLock 的优势主要表现在支持乐观读取机制条件等待。所以在写操作频繁而读操作较少存在大量的锁竞争的情况下,直接使用悲观读锁,性能跟ReentrantReadWriteLock是相差不大的。

  • 使用方法

    import java.util.concurrent.locks.StampedLock;
    
    public class StampedLockExample {
        private Object SharedData;
        private final StampedLock lock = new StampedLock();
    
        public void write(Object obj) {
            //获取写锁
            long stamp = lock.writeLock();
            try {
                // 执行写业务逻辑,如下
                SharedData = obj;
            } finally {
                // 释放写锁
                lock.unlockWrite(stamp);
            }
        }
    
        // 完全使用乐观读锁
        public Object read1() {
            while(true){
                Object result = null;
                
                // 获取乐观读锁
                long stamp = lock.tryOptimisticRead(); 
                // 执行读业务逻辑,如下
                result = SharedData;
                // 验证共享数据是否被修改,如果没有被修改过,则直接返回。否则重新读
                if (lock.validate(stamp)) {
    		      return result;
                }
            }
        }
        
        // 使用乐观读锁 + 非观读锁
        public Object read2() {
            Object result = null;
            
            // 获取乐观读锁
            long stamp = lock.tryOptimisticRead(); 
            // 执行读业务逻辑(这里先读一次),如下
            result = SharedData;
            
            // 验证共享数据是否被修改
            if (lock.validate(stamp)) {
                // 如果共享数据没有被修改过,则直接使用
                return result;
            }
            
            // 获取悲观读锁
            stamp = lock.readLock();
            try {
                // 执行读业务逻辑,重新读
                result = SharedData;
            } finally {
                // 释放写锁
                lock.unlockRead(stamp);
            }
            
            return result;
        }
    }
    

十二、Semaphore 信号量/限流/同步辅助类

  • 详细使用说明请看Java 多线程之 Semaphore

  • Semaphore(信号量)是一种并发控制机制,用于控制对共享资源的访问。它维护了一个计数器,可以限制同时访问某个资源的线程数量。常用于限制同时访问某个资源的线程数量,例如控制数据库连接池的并发访问、控制线程池的并发任务数、生产者-消费者问题、读者-写者问题等。

  • 使用方法

    public class SemaphoreExample {
        private Semaphore semaphore = new Semaphore(5); // 允许同时访问资源的线程数量,这里设置为5,表示可以有5个线程同时访问
    
        public void accessResource() {
            try {
                semaphore.acquire(); // 获取许可证,如果有许可证,则计数器减1;如果没有可用许可证,则阻塞
                // 访问共享资源的代码
            } catch (InterruptedException e) {
                // 处理中断异常
            } finally {
                semaphore.release(); // 释放资源,计数器加1
            }
        }
    }
    

十三、Exchanger 数据交换,同步辅助类

  • 详细使用说明请看Java 多线程之 Exchanger

  • Exchanger(交换器)是Java并发包中的一个工具类,用于两个线程之间交换数据。它提供了一个同步点,当两个线程都到达该点时,它们可以交换数据,并且在交换完成后继续执行。

  • 使用方法:

    import java.util.concurrent.Exchanger;
    
    public class ExchangerExample {
        private Exchanger<String> exchanger = new Exchanger<>();
    
        // 线程1
        public void thread1() {
            try {
                // 执行线程1的业务逻辑
                String data1 = "线程1的业务数据";
                String exchangedData = exchanger.exchange(data1);
                // exchangedData 是线程2的业务数据,在这里可以继续处理交换后的数据
                
                // 执行线程1的业务逻辑
            } catch (InterruptedException e) {
            }
        }
    
        // 线程2
        public void thread2() {
            try {
                // 执行线程2的业务逻辑
                String data2 = "线程2的业务数据";
                String exchangedData = exchanger.exchange(data2);
                // exchangedData 是线程1的业务数据,在这里可以继续处理交换后的数据
                
                // 执行线程2的业务逻辑
            } catch (InterruptedException e) {
            }
        }
    }
    

十四、LockSupport 阻塞和唤醒线程

  • 详细使用说明请看Java 多线程之 LockSupport

  • LockSupport 是Java并发包中的一个工具类,用于线程的阻塞和唤醒。它提供了一种基于线程的许可(permit)的方式来实现线程的阻塞和唤醒,而不需要显式地使用锁。例如某个条件满足后阻塞线程,然后等待某个条件满足后再继续执行、实现线程间的协作等。

  • 使用方法

    import java.util.concurrent.locks.LockSupport;
    
    public class LockSupportExample {
        public static void main(String[] args) {
            
            Thread thread1 = new Thread(() -> {
                // 执行业务逻辑
                
                LockSupport.park(); // 阻塞当前线程
                
                // 继续执行业务逻辑
            });
            thread1.start();
            
            try {
                Thread.sleep(2000); // 等待2秒钟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(thread1); // 唤醒 thread1 线程
        }
    }
    

文章来源:https://blog.csdn.net/qifu123/article/details/134916119
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。