Synchronized关键字

Java 中的 synchronized 是一种常见的同步机制,用于实现线程间的互斥访问,确保共享资源在多线程环境中的操作是安全的。

作用:

  1. 保证线程安全:

    • 保证在多线程的环境下,同一时间只有一个线程可以访问被synchronized修饰的代码块或者方法
  2. 实现原子性:

    • 防止线程切换造成的中间状态问题
  3. 可见性:

    • 线程对共享变量的修改对其他线程可见。

使用方法

Java 中 synchronized 关键字有三种主要使用方式:

1.修饰实例方法

同步当前实例对象 (this) 的方法。

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

特点:

  • 同步的是整个方法,锁定当前实例对象。
  • 同一对象的不同线程调用此方法时,必须等待当前线程释放锁。
2.修饰静态方法

同步当前类 (Class 对象)。

public class Counter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static synchronized int getCount() {
        return count;
    }
}

特点:

  • 锁定的是 Class 对象,而非实例。
  • 所有访问此类的线程会竞争同一个锁。
3.修饰代码块

同步代码块,指定锁对象。

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

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

特点:

  • 可以灵活控制锁定的范围,减少锁的粒度,提高并发效率。
  • 锁对象可以是任意对象,如 this、类对象 (Class) 或自定义的锁对象。

锁粒度

锁的粒度

  • 粗粒度:

    • 锁定方法或较大的代码块,容易导致线程阻塞,性能下降。
  • 细粒度:

    • 锁定更小的代码块,提高线程并发性,但需要小心死锁问题。

synchronized 的底层原理

1.JVM 实现:
  • synchronized 是基于 JVM 实现的,依赖 monitor 对象。
  • 每个对象都有一个关联的监视器锁 (monitor),当线程获取锁时,进入 monitor 的 ENTRY。
2.字节码层面
  • synchronized 方法会在字节码中标记为 ACC_SYNCHRONIZED。
  • synchronized 代码块使用 monitorenter 和 monitorexit 指令。
3.重量级锁到轻量级锁的优化:
  • 偏向锁:如果只有一个线程竞争,锁会偏向该线程。
  • 轻量级锁:当有少量线程竞争时,通过自旋锁避免线程阻塞。
  • 重量级锁:竞争激烈时,升级为重量级锁,阻塞线程。

线程之间的同步于互斥

互斥:

  • synchronized 确保同一时间只有一个线程进入临界区。

同步:

  • 线程对共享变量是可见的

注意事项

  1. 避免死锁:

    * 多线程中,如果两个线程互相持有对方需要的锁,可能导致死锁。
  2. 性能问题:

    * 粒度太大的锁会降低并发性能。尽量缩小同步范围。
  3. 适配场景:

    * 对读多写少的场景,可以考虑 ReentrantReadWriteLock 替代 synchronized。
    

ReentrantLock

特性

  1. 可重入性:同一线程可以多次获取同一把锁,而不会发生死锁(与 synchronized 行为一致)。
  2. 公平锁和非公平锁

    • 公平锁:线程按先后顺序获取锁,先等待的线程优先。
    • 非公平锁(默认):可能会有线程插队获取锁,提升吞吐量。
  3. 尝试锁(tryLock)

    • 支持尝试获取锁而不会导致线程阻塞。
  4. 中断锁

    • 可以通过 lockInterruptibly 方法在获取锁时响应中断。
  5. 条件变量(Condition)

    • 可以通过 newCondition() 创建条件变量,用于实现线程的精确等待和唤醒。

基本用法

  1. 简答的加锁
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void criticalSection() {
        lock.lock(); // 获取锁
        try {
            System.out.println(Thread.currentThread().getName() + " is in critical section");
            Thread.sleep(1000); // 模拟操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 确保最终释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable task = example::criticalSection;

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");

        t1.start();
        t2.start();
    }
}
  1. 公平锁与非公平锁
import java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
    private final ReentrantLock lock;

    // 构造函数,指定公平锁或非公平锁
    public FairLockExample(boolean fair) {
        this.lock = new ReentrantLock(fair);
    }

    public void criticalSection() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " acquired lock");
            Thread.sleep(1000); // 模拟操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        FairLockExample example = new FairLockExample(true); // true表示公平锁

        for (int i = 1; i <= 5; i++) {
            new Thread(example::criticalSection, "Thread-" + i).start();
        }
    }
}
  1. 尝试锁(tryLock)
import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void tryLockExample() {
        if (lock.tryLock()) { // 尝试获取锁
            try {
                System.out.println(Thread.currentThread().getName() + " acquired lock");
                Thread.sleep(1000); // 模拟操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " could not acquire lock");
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();

        Thread t1 = new Thread(example::tryLockExample, "Thread-1");
        Thread t2 = new Thread(example::tryLockExample, "Thread-2");

        t1.start();
        t2.start();
    }
}

4.响应中断

lockInterruptibly 允许线程在等待锁时可以响应中断。

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void criticalSection() {
        try {
            lock.lockInterruptibly(); // 可中断的锁
            System.out.println(Thread.currentThread().getName() + " acquired lock");
            Thread.sleep(2000); // 模拟操作
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " was interrupted");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        InterruptibleLockExample example = new InterruptibleLockExample();

        Thread t1 = new Thread(example::criticalSection, "Thread-1");
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(500);
                t1.interrupt(); // 中断 t1
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
    }
}
  1. 条件变量(Condition)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void await() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " waiting");
            condition.await(); // 线程进入等待状态
            System.out.println(Thread.currentThread().getName() + " resumed");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " signaling");
            condition.signal(); // 唤醒等待线程
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionExample example = new ConditionExample();

        Thread t1 = new Thread(example::await, "Thread-1");

        t1.start();

        Thread.sleep(2000);

        Thread t2 = new Thread(example::signal, "Thread-2");
        t2.start();
    }
}

ReentrantLock和synchronized的对比

特性 ReentrantLock synchronized
锁的属性 显式锁 隐式锁
可中断 支持 不支持
尝试锁 支持 不支持
公平性 支持 不支持,仅非公平
条件变量 支持 不支持
性能 高并发性能由于synchronized 一般场景性能较好

锁的升降级

锁的升降级和降级是与 Java 锁的实现(如 synchronized 和 ReentrantLock) 以及 JVM 的优化策略(如偏向锁、轻量级锁、重量级锁)密切相关的。以下是锁的升降级与降级的规则和机制:

锁的级别

Java 中的锁根据性能和功能的不同,通常分为以下几种级别:

  • 无锁(No Lock):线程之间不涉及竞争。
  • 偏向锁(Biased Lock):一个线程获取锁后,锁会偏向该线程,减少加锁和解锁的代价。
  • 轻量级锁(Lightweight Lock):多线程竞争时,采用自旋的方式尝试获取锁,避免线程上下文切换的开销。
  • 重量级锁(Heavyweight Lock):当自旋失败或竞争激烈时,锁会升级为重量级锁,涉及线程挂起和唤醒。

锁的升级规则

锁升级是 JVM 的一种优化策略,自动进行、不可逆。升级规则如下:

  1. 无锁 → 偏向锁:

    • 如果锁对象是初次被一个线程访问,JVM 会将该对象标记为偏向锁,线程以后访问时无需加锁操作。
    • 偏向锁在没有竞争的情况下效率极高。
  2. 偏向锁 → 轻量级锁:

    • 当有其他线程尝试获取偏向锁时,偏向锁会被撤销。
    • 进入轻量级锁阶段,多个线程尝试自旋获取锁。
  3. 轻量级锁 → 重量级锁:

    • 当自旋的线程数量较多(超过一定次数,默认 10 次)或竞争激烈时,轻量级锁会升级为重量级锁。
    • 升级为重量级锁后,线程会被挂起,避免进一步的 CPU 消耗。

锁的降级规则

锁的降级是 JVM 的另一种优化策略,用来降低锁的开销。降级规则如下:

  1. 重量级锁 → 轻量级锁:不会发生。

    • 锁的升级是不可逆的,重量级锁不能回退到轻量级锁。
  2. 轻量级锁 → 偏向锁:

    • 锁没有被释放,且竞争已经结束时,JVM 会尝试将锁恢复为偏向锁。
    • 这个过程通常在偏向锁的重新分配机制中发生。
  3. 偏向锁 → 无锁:

    • 偏向锁释放后,如果后续没有线程访问锁对象,则锁可能恢复到无锁状态。

原子性

原子操作是指一个不可中断的操作,保证操作的完整性,即使在多线程环境中也不会被其他线程干扰。换句话说,原子操作要么全部完成,要么全部失败,中间不会有任何状态暴露。

Java 的原子性

  • 定义

原子性是多线程编程中的一种重要性质,用于描述某些操作在执行期间不会被线程切换中断。这是线程安全的基础之一。

Java 中的原子操作实现方式

(1)使用 synchronized

synchronized 关键字可以确保一个代码块或方法是原子的。它通过线程排队和锁机制,保证同一时刻只有一个线程能够执行同步代码块。

public synchronized void increment() {
    i++;
}

缺点:

  • 上下文切换开销较大。
  • 多线程竞争时可能导致性能下降。
(2)使用 java.util.concurrent.atomic 包

Java 的 java.util.concurrent.atomic 包提供了一组线程安全的原子类,如 AtomicInteger、AtomicLong 等,内部使用 CAS (Compare-And-Swap) 实现高效的原子操作。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private static final AtomicInteger atomicInt = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                int newValue = atomicInt.incrementAndGet(); // 原子性自增
                System.out.println("Thread " + Thread.currentThread().getName() + ": " + newValue);
            }).start();
        }
    }
}

常用方法:

  • incrementAndGet():自增并返回新值。
  • decrementAndGet():自减并返回新值。
  • compareAndSet(expect, update):如果当前值等于预期值,则更新为新值。
(3)使用锁(Lock 和 ReentrantLock)

Java 的 java.util.concurrent.locks.Lock 接口提供了一种显式锁机制,可以更细粒度地控制线程之间的访问

import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

优点:

  • 可以中断等待锁的线程(lockInterruptibly)。
  • 更灵活的锁定机制,比如非阻塞获取锁 (tryLock)。

(4)Volatile 与原子性

volatile 修饰符只能保证 可见性 和 禁止指令重排,但不能保证操作的原子性。例如,以下代码不是线程安全的:

private volatile int count = 0;

public void increment() {
    count++;
}

原子操作底层原理

Java 中的原子操作通常是通过 CPU 指令和底层内存模型实现的,核心机制包括

  • CAS(Compare-And-Swap):
    CAS 是一种硬件级别的原子操作,通过比较变量当前值和预期值是否相等来决定是否更新。如果相等,则执行更新操作;否则,重试直到成功。
  • CPU 指令支持:
    大多数现代处理器提供支持原子操作的指令,例如 x86 架构中的 lock cmpxchg。
  • 内存屏障(Memory Barrier):
    保证线程对共享变量的修改对其他线程可见,并禁止编译器和 CPU 指令重排序。

应用场景

  • 计数器: 使用 AtomicInteger 实现并发计数器。
  • 多线程环境下的 ID 生成: 确保唯一性。
  • 无锁数据结构: 使用原子类实现高效、线程安全的队列或栈。

voliate

volatile 是 Java 中的一个修饰符,用于修饰变量,保证 可见性 和 有序性,但不能保证操作的 原子性。它主要用于多线程环境下的变量共享,确保一个线程对变量的修改对其他线程立即可见。

作用

  1. 可见性
    当一个线程修改了 volatile 修饰的变量,新的值会立即被刷新到主内存中。其他线程读取该变量时会直接从主内存获取最新值,而不是从自己的工作内存中读取过期的缓存值。
  2. 有序性
    volatile 可以防止指令重排序,确保在它之前的操作不会被编译器或 CPU 调整到它之后。

等待/通知机制

等待/通知机制是多线程编程中的一种重要通信方式,主要用于线程之间协调工作。通过一个线程等待条件满足,另一个线程在条件满足时通知,避免忙等(不断轮询消耗 CPU 资源)。

在 Java 中,等待/通知机制依赖于 Object 类的以下三个方法:

  • wait():让当前线程进入等待状态,释放锁,并等待其他线程的通知。
  • notify():唤醒一个正在等待同一对象的线程。
  • notifyAll():唤醒所有正在等待同一对象的线程。

这些方法必须在同步块(synchronized)中调用,因为它们需要先获得对象的监视器锁。

基本原理

  • 等待线程:
    调用 wait() 后,线程释放当前对象的锁,并进入等待队列。
  • 通知线程:
    调用 notify() 或 notifyAll() 后,从等待队列中唤醒一个或全部线程,使它们尝试重新获得锁并继续执行。

代码示例:等待/通知

class Buffer {
    private int data = 0;          // 缓冲区数据
    private boolean hasData = false; // 缓冲区状态标记

    public synchronized void produce() throws InterruptedException {
        while (hasData) {          // 缓冲区满时等待
            wait();
        }
        data++;                    // 生产数据
        System.out.println("Produced: " + data);
        hasData = true;
        notify();                  // 通知消费者
    }

    public synchronized void consume() throws InterruptedException {
        while (!hasData) {         // 缓冲区为空时等待
            wait();
        }
        System.out.println("Consumed: " + data);
        hasData = false;
        notify();                  // 通知生产者
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();

        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.produce();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.consume();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

wait 和 notify 的注意事项

  1. 必须在同步块或方法内调用:

    • 调用 wait() 或 notify() 的对象必须是当前线程持有的锁对象。
    • 否则,会抛出 IllegalMonitorStateException 异常。
  2. 释放锁:

    • wait() 会释放当前对象的锁,但并不会释放其他锁。
    • 唤醒后,线程需重新竞争锁才能继续执行。
  3. 避免虚假唤醒:

    • 使用 while 循环检查条件,而不是 if,以防止虚假唤醒。
  4. 线程间通信必须基于同一对象:

    • wait() 和 notify() 调用的对象应该是同一个,否则线程无法正确协调。

notify 和 notifyAll 的区别

方法作用
单元 1唤醒一个正在等待同一对象锁的线程,线程具体由 JVM 选择。
单元 3唤醒所有正在等待同一对象锁的线程,优先级高的线程先执行。

建议:如果只有一个线程在等待,可以用 notify;多个线程等待时,用 notifyAll 以避免线程饥饿。

等待/通知机制 VS 其他同步工具

  • wait/notify:是较底层的机制,需要手动管理同步逻辑。
  • Lock 和 Condition:是更高级的工具,提供了更加灵活的线程等待与通知机制。
  • BlockingQueue:是高层抽象,内置生产者/消费者模式,无需显式调用等待或通知。

ThreadLocal

ThreadLocal 是 Java 提供的一种线程本地存储机制,用于为每个线程创建一个独立的变量副本。每个线程可以独立地操作自己的副本,互不影响,适合在多线程环境下实现线程隔离。

ThreadLocal 的主要方法

  1. get():

    • 获取当前线程绑定到 ThreadLocal 的值。
    • 如果当前线程没有值,则返回 null 或初始化的默认值。
  2. set(T value):

    • 将值绑定到当前线程的 ThreadLocal。
  3. remove():

    • 移除当前线程绑定的值,避免内存泄漏。
  4. initialValue():

    • 定义当前线程值的初始值,默认返回 null,可通过继承 ThreadLocal 并重写此方法实现。

ThreadLocal 的应用场景

  1. 用户信息隔离:在 Web 应用中保存每个线程对应的用户会话信息。
  2. 数据库连接管理:为每个线程分配独立的数据库连接。
  3. 事务管理:在多线程中保存当前事务上下文。
  4. 线程安全对象:如 SimpleDateFormat。

示例

  1. 使用 ThreadLocal 管理用户信息
public class UserContext {
    private static ThreadLocal<String> userThreadLocal = ThreadLocal.withInitial(() -> "Unknown User");

    public static void setUser(String user) {
        userThreadLocal.set(user);
    }

    public static String getUser() {
        return userThreadLocal.get();
    }

    public static void removeUser() {
        userThreadLocal.remove();
    }
}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            UserContext.setUser("User1");
            System.out.println(Thread.currentThread().getName() + " - " + UserContext.getUser());
            UserContext.removeUser();
        });

        Thread t2 = new Thread(() -> {
            UserContext.setUser("User2");
            System.out.println(Thread.currentThread().getName() + " - " + UserContext.getUser());
            UserContext.removeUser();
        });

        t1.start();
        t2.start();
    }
}

ThreadLocal 内存泄漏问题

原因
  • ThreadLocal 的值存储在 Thread 的 ThreadLocalMap 中,使用的是弱引用。
  • 如果线程存活时间较长,而没有手动调用 remove(),则可能导致内存泄漏。
解决方式
  • 显式调用 remove():

ThreadLocal工作流程

  1. 内部实现结构:

    • 每个线程对象 (Thread) 都有一个 ThreadLocalMap。
    • ThreadLocalMap 是一个以 ThreadLocal 为键的弱引用键值对。
  2. 存储过程:

    • 当调用 threadLocal.set(value) 时,当前线程的 ThreadLocalMap 会保存 ThreadLocal -> value 的映射。
  3. 获取值:

    • 调用 threadLocal.get() 时,通过当前线程的 ThreadLocalMap 找到对应的值。
  4. 清理:

    • 因为 ThreadLocal 的键是弱引用,当 ThreadLocal 被回收后,ThreadLocalMap 的值无法再被访问,但值仍然会占用内存,需手动调用 remove()。

ThreadLoacl优缺点

优点
  1. 线程隔离:每个线程独立持有变量,避免数据共享导致的线程安全问题。
  2. 简单易用:在特定场景下代替复杂的同步机制。
缺点
  1. 内存泄漏风险:长时间运行的线程未清理 ThreadLocal 值会造成内存泄漏。
  2. 生命周期管理复杂:需要显式调用 remove(),否则容易出错。

ReentrantReadWriteLock

ReentrantReadWriteLock 是 Java 并发包 (java.util.concurrent.locks) 提供的一个读写锁实现,允许多个读线程同时访问,但在写线程访问时,所有线程(包括读线程)都会被阻塞。

组成

  1. 读锁 (ReadLock):

    • 支持多个线程并发读取。
    • 前提是没有线程持有写锁。
    • 使用场景:数据只读时,提升并发性能。
  2. 写锁(WriteLock)

    • 独占锁,同时只允许一个线程持有。
    • 在写锁持有期间,其他线程(包括读线程和写线程)都会被阻塞。
    • 使用场景:需要修改数据时,确保线程安全。

主要方法

  1. 读锁相关方法
  • lock():获取读锁,如果当前有写锁,会阻塞。
  • tryLock():尝试获取读锁,立即返回结果。
  • unlock():释放读锁。
  1. 写锁相关方法
  • lock():获取写锁,其他线程必须等待释放后才能获取。
  • tryLock():尝试获取读锁,立即返回结果。
  • unlock():释放写锁。
  1. 状态查询方法
  • getReadLockCount():当前持有读锁的线程数。
  • isWriteLocked():写锁是否被某线程持有。
  • getWriteHoldCount():当前线程持有写锁的次数(重入次数)。
  • getReadHoldCount():当前线程持有读锁的次数。

特性

  1. 支持可重入

    • 持有锁的线程可以重复获取同一个锁。
    • 支持读锁、写锁的分别重入。
  2. 写锁降级为读锁

    • 允许持有写锁的线程获取读锁,这称为锁降级。
    • 锁降级通过以下步骤完成:

      • 先获取写锁。
      • 再获取读锁。
      • 最后释放写锁。
  3. 写锁不能升级为读锁

    • 持有读锁的线程尝试获取写锁会导致死锁。

优势

  1. 提高读性能:

    • 允许多个读线程并发读取,适合读多写少的场景。
  2. 读写分离:

    • 写线程不会阻塞读线程,除非写线程持有锁。
  3. 线程安全:

    • 提供线程级别的独占性和互斥控制。

注意事项

  1. 锁饥饿问题:

    • 如果有大量的读线程,可能导致写线程长期等待写锁。
  2. 性能开销:

    • 对于简单的线程竞争场景,ReentrantLock 可能比 ReentrantReadWriteLock 性能更优。
  3. 锁降级:

    • 推荐在写操作后立刻降级为读锁,提高读操作性能。

LockSupport

LockSupport 是 Java 并发包中的一个工具类,主要用于线程的阻塞和唤醒。它提供了一种灵活而高效的线程同步机制,相较于 Object.wait() 和 Object.notify(),LockSupport 更加底层和轻量。

核心方法

LockSupport 的核心是两个静态方法:park() 和 unpark(Thread thread)。

1.park()

  • 使当前线程阻塞,直到被唤醒。
  • 阻塞状态是基于许可(permit)的。每个线程都有一个与之关联的许可,park() 会消费许可。
  • 如果许可不可用,则线程会阻塞;如果许可已存在,则调用 park() 不会阻塞线程。

2.unpark(Thread thread)

  • 唤醒指定线程,使其可以从 park() 状态中恢复。
  • 如果目标线程尚未被 park(),则 unpark 会预先授予一次许可。
  • 每个线程最多只能持有一个许可,重复调用 unpark() 不会累计。

基本用法

防止死锁的公平队列实现

import java.util.concurrent.locks.LockSupport;

class FairLock {
    private Thread currentThread = null;

    public void lock() {
        Thread thread = Thread.currentThread();
        while (currentThread != null) {
            LockSupport.park(); // 挂起当前线程
        }
        currentThread = thread; // 获取锁
    }

    public void unlock() {
        if (Thread.currentThread() == currentThread) {
            currentThread = null;
            LockSupport.unpark(Thread.currentThread()); // 唤醒等待线程
        }
    }
}

注意事项

1.许可特性:

* unpark() 提供的许可是一次性的,且不会累计。重复调用 park() 会阻塞线程。

2.避免死锁:

* 如果线程在 park() 之前没有调用 unpark(),线程会一直阻塞。因此,需要确保调用 unpark() 的逻辑不会遗漏。

3.中断兼容:

* park() 会响应线程的中断状态。如果线程被中断,park() 不会阻塞,但不会抛出 InterruptedException。

4.线程安全:

* LockSupport 的操作是线程安全的,可以在多线程环境下安全地使用。

应用场景

  1. 阻塞队列的实现
    在高性能阻塞队列如 LinkedBlockingQueue 中,LockSupport 常用于线程等待和唤醒。
  2. 线程池实现
    Java 中的线程池使用 LockSupport 来管理线程的挂起和激活。
  3. 自定义锁
    基于 LockSupport 可以实现更加灵活的锁机制,如公平锁或非公平锁。
  4. JUC(Java并发包)工具类
    LockSupport 是很多 JUC 工具类的基础,如 CountDownLatch、Semaphore、ReentrantLock 等。

阻塞队列

阻塞队列是一种支持线程阻塞的队列,当队列为空或已满时,操作队列的线程会自动阻塞,直到条件满足后被唤醒。

  1. 主要功能
  2. 当队列为空,获取操作(take())会阻塞,直至队列之中有元素为止
  3. 当队列已满,插入操作(put())会阻塞,直至队列有空闲空间为止

阻塞队列的应用场景

  1. 生产-消费者模型

    • 生产者不断将数据加入队列中,消息从队列中取出
    • 阻塞队列自动处理生产者和消费者的速度差,避免繁琐的锁操作
  2. 线程池任务队列

    • 在线程池中,任务会被提交到阻塞队列之中
  3. 限流控制

    • 通过限制队伍容量,可以有效控制数据处理的速度,防止系统过载

阻塞队列的实现

Java 提供了多种阻塞队列实现,主要位于 java.util.concurrent 包中。

  • ArrayBlockingQueue:有界队列,基于数组,容量固定
  • LinkedBlockingQueue:可选有界队列,基于链表,适合数据流入和流出速度不同的场景
  • PriorityBlockingQueue: 基于优先级的无界队列,元素按照优先级排列,但插入操作稍慢
  • DeleyQueue:储存接口实现了Delayed接口的元素,只有到期以后才能被取出
  • SynchronuosQueue:不存储元素,每次插入操作必须等待相应的移除操作完成,适合高同步场景

阻塞队列的基本方法

  1. 插入元素

    • put(E e): 如果队列已满,线程阻塞直到有空闲空间为止
    • offer(E e,long timeout,TimeUnit nuit): 尝试在指定的时间内插入,失败返回false
  2. 获取元素

    • take():如果队列为空,阻塞直到有可用元素
    • poll(long time, TimeUnit unit):尝试在指定时间内获取元素,超时返回 null。
  3. 其他方法

    • size():获取队列当中的元素个数
    • remainingCapacity() :获取队列剩余容量

阻塞队列的实现原理

  • 基于锁和条件变量

    • 阻塞队列通常使用重入锁(ReentrantLock)和条件变量(Condition)实现线程的阻塞与唤醒
    • 插入和获取操作分别有条件变量notEmpty和notFull,用于在条件不满足时阻塞
  • 无锁实现(如 ConcurrentLinkedQueue):

    • 部分无界队列使用 CAS(Compare-And-Swap) 操作实现无锁线程安全。
    • CAS 能确保操作的原子性,提高并发性能。
阻塞队列的优缺点
  • 优点:
  • 自动管理线程同步,避免显式锁的使用
  • 支持多线程高效并发,性能优越
  • 提供多种实现,满足不同场景的需求(有界,无界,优先级)
  • 缺点:
  • 在高负载的情况下会出现队列耗尽或者插入过慢的情况
  • 部分阻塞队列的性能可能低于无锁实现

说一说CAS

CAS(Compare-And-Swap)的底层实现主要依赖于 CPU 的硬件支持,通过特定的原子指令完成操作,从而确保线程安全性

底层原理

CAS 是一种 乐观锁机制,它基于“先比较,后交换”的思想,完成并发情况下的变量更新。
它需要三个参数:

  • V:内存中的变量值(预期值)。
  • E:期望值(Expected value)。
  • N:需要更新的新值(New value)。

过程:

  1. 如果变量的当前值 V 等于期望值 E,则将变量更新为新值 N。
  2. 如果 V 不等于 E,说明变量已被其他线程修改过,则不进行更新。

这种方式通过硬件指令保证整个操作的原子性。

CAS 底层实现

在Java中,CAS是由Unsafe类的本地方法实现的,其核心是依赖于CPU提供的原子性操作指令(如x86的cmpxchg)。

实现方式:

  1. CPU指令支持

    • 在 x86 架构下,CAS 是通过 cmpxchg(Compare and Exchange)指令实现的。
    • 该指令对一个内存地址的值进行比较和条件性替换,这个过程是原子的
  2. Unsafe 类

    • Java 提供了 sun.misc.Unsafe 类作为 CAS 操作的入口。
    • Unsafe 类的方法 compareAndSwapInt、compareAndSwapLong、compareAndSwapObject 分别对整型、长整型和对象引用进行原子操作。
  3. JNI 实现

    • compareAndSwapXxx 是用 JNI 调用底层的 C/C++ 实现的,与硬件直接交互。
    • 在底层,它会调用 cmpxchg 或其他架构对应的原子性指令。

CAS 的优缺点

优点:

  1. 无锁操作:避免了线程阻塞,提高了性能。
  2. 原子性:由硬件保证 CAS 操作的原子性,线程安全。
  3. 高效:尤其在冲突较少的情况下性能优势明显。

缺点:

  1. ABA 问题:

    • 如果一个变量的值从 A 改成 B,又从 B 改回 A,CAS 无法识别这种情况。
    • 解决方法:可以结合 版本号 或 引用对象+版本号的 AtomicStampedReference。
  2. 自旋消耗 CPU:

    • 如果 CAS 操作失败,会不断重试,消耗 CPU 资源。
  3. 只能保证单个变量的原子性:

    • 对于多个变量的操作,需要使用锁或者其他机制。

并发容器

常见的并发容器

  1. ConcurrentHashMap
  • 用途:线程安全的哈希表,用于高效地存储和访问键值对。
  • 底层实现:

    • Java 7:

      • 基于分段锁实现,将数据分成多个段,每个段是一个小哈希表,拥有自己的锁,减少锁的争用
      • 默认分为16段
    • Java 8:

      • 使用 CAS 和 synchronized代替分段锁
      • 数据结构是数组 + 链表 + 红黑树
      • 当链表长度超过阈值(8)时,会转换为红黑树,降低时间复杂度
    • 读操作:使用voliate关键字和CAS确保操作无锁
    • 写操作:采用 CAS 或 synchronized,必要时对单个桶加锁,减少锁粒度。
  1. CopyOnWriteArrayList
  • 用途:线程安全的动态数组,适用于读多写少的场景
  • 底层实现:

    • 所有修改操作(如 add、remove)会复制一份底层数组,操作完成后会用新数组替换原数组
    • 读操作可以无锁进行,因为读操作直接使用快照数组
    • 修改的性能较差,因需创建新数组,但读性能优异。
  1. CopyOnWriteArraySet
  2. 用途:线程安全的 Set 实现,基于 CopyOnWriteArrayList。
  3. 底层实现:

    • 使用CopyOnWriteArrayList存储元素
    • 写入元素的时候会检查是否存在重复元素,避免重复
  4. ConcurrentSkipListMap 和 ConcurrentSkipListSet
  5. 用途:线程安全的有序 Map 和 Set,基于跳表实现,支持高效的范围查找和排序。
  6. 底层实现:

    • 使用跳表(Skip List)数据结构。
    • 插入、删除和搜索操作通过 CAS 实现线程安全。
    • 跳表通过多层索引提升查找性能,平均时间复杂度为 O(log N)。
  7. BlockingQueue(ReentrantLock + Condition)
  8. 用途:线程安全的队列,适合生产者-消费者模式
  9. 实现类:

    • ArrayBlockingQueue:

      • 基于数组的有界阻塞队列,使用单一锁机制。
    • LinkedBlockingQueue

      • 基于链表的阻塞队列,分别为生产者和消费者设置锁,减少锁竞争
    • PriorityBlockingQueue:

      • 基于堆实现的优先级队列,无届队列
    • SynchronousQueue:

      • 不存储元素,生产者和消费者必须直接交接数据。
    • DelayQueue

      • 基于 PriorityQueue 实现的队列,元素到期后才能取出
  10. 底层实现:

    • 使用 ReentrantLock 和 Condition 实现线程安全和阻塞。
    • 生产者使用 notFull 条件变量,消费者使用 notEmpty 条件变量。
  11. ConcurrentLinkedQueue和ConcurrentLinkedDeque
  12. 用途:线程安全的无界非阻塞队列(Queue)和双端队列(Deque)。
  13. 底层实现:

    • 使用 CAS 和链表实现。
    • 每次入队或出队操作都会使用 CAS 更新头节点或尾节点。
    • 避免了锁的使用,适合高并发场景。

常见容器底层实现机制

  1. CAS
  2. 锁机制

    • 分段锁:Java7 ConcurrentHashMap 一般分为16段
    • ReentrantLock:支持公平锁和非公平锁,细化锁粒度,提高性能。
    • synchronized: Java8 ConcurrentHashMap偶尔使用
  3. volatile

    • 用于保证变量的可见性
  4. Condition

    • 用于阻塞队列
  5. Copy-on-Write

    • 开辟新的数组供数据写,修改操作基于数组快照,避免了锁的竞争。
  6. 跳表

    • 使用跳表代替红黑树,利用多层索引实现高效查找

Fork/Join

Java 7 中引入的一种用于并行编程的框架,专为分治任务而设计。它在 java.util.concurrent 包中提供,允许程序将大任务拆分成多个小任务,并将这些小任务分配到多个线程中执行,从而充分利用多核 CPU 的并行计算能力。

Fork/Join 框架的核心概念

  1. 分治思想
  • Fork/Join 框架基于 分治法:

    • 分解(Fork):将复杂的大任务拆分成若干小任务。
    • 处理(Compute):如果任务足够小,直接计算。
    • 合并(Join):将小任务的结果合并,形成最终结果。
  1. ForkJoinPool

    • ForkJoinPool是Fork/Join框架的执行器
    • 它管理一组工作线程,通过一种工作窃取算法来高效执行任务
    • 每个线程都有一个双端队列,用于存储要执行的任务
  2. ForkJoinTask

    • ForkJoinTask 是任务的基本单元,是一个抽象类
    • 子类

      • RecursiveTask:用于有返回值的任务
      • RecursiveAction:用于无返回值的任务

Fork/Join 框架的工作流程

  1. 主任务(ForkJoinTask)被提交到 ForkJoinPool。
  2. 主任务通过 fork() 方法将任务拆分成子任务,子任务进一步拆分直到足够小。
  3. 每个子任务被分配给线程处理,任务完成后通过 join() 方法合并结果。
  4. 如果线程空闲,会尝试从其他线程的队列中“窃取”任务以提高效率(Work-Stealing)。

优势

  1. 高效利用多核 CPU:通过分治法和工作窃取算法最大化线程利用率。
  2. 任务动态分配:通过工作窃取算法解决负载不均问题。
  3. 简单易用:开发者只需专注于任务的拆分和合并逻辑。

缺点

  1. 适用范围有限:适合计算密集型任务,不适合 I/O 密集型任务。
  2. 任务拆分开销:过多的小任务可能导致性能下降。
  3. 调试复杂性:并行化后的代码调试和排查问题较为复杂。

工作窃取算法

工作窃取算法是 ForkJoinPool 的核心优化机制:

  • 每个线程有一个双端队列,存储它的任务。
  • 如果线程完成了自己的任务,会从其他线程的队列尾部窃取任务,以减少空闲时间。
  • 这种方式有效平衡了线程之间的负载,提升了性能。

CountDownLatch

CountDownLatch 是 Java 并发工具包(java.util.concurrent)中提供的一个同步辅助类,用于管理多个线程之间的协作。它通过一个计数器实现,线程可以等待计数器变为零再继续执行,或者通过减少计数器的值来释放其他线程的等待。

主要方法

  1. CountDownLatch(int count)

    • 创建一个 CountDownLatch 实例,count 是计数器的初始值。
  2. void await()

    • 让当前线程等待,直到计数器为零,或者线程被中断。
  3. void countDown()

    • 减少计数器的值。如果计数器变为零,所有调用了 await() 的线程会被唤醒。
  4. long getCount()

    • 获取当前计数器的值(通常用于监控或调试)。

工作原理

  • 初始化时设置计数器的值(比如 new CountDownLatch(3))。
  • 主线程调用 await() 方法进入等待状态。
  • 每个工作线程在完成任务后调用 countDown() 将计数器减一。
  • 当计数器减为零时,所有调用 await() 的线程会被唤醒,继续执行。

应用场景

  1. 线程启动协调:主线程等待多个线程完成初始化后再执行任务。
  2. 并行任务等待:多个任务并行执行,主线程等待所有任务完成后汇总结果。
  3. 控制服务启动顺序:多个服务按照依赖关系启动,只有当依赖的服务启动完成后,才能启动当前服务。

注意事项

  1. 计数器只能减,不能加:CountDownLatch 的计数器是一次性的,无法重置或增加。
  2. 线程安全:countDown() 和 await() 是线程安全的,可以在多个线程中调用。
  3. 不能重复使用:如果需要复用,建议使用 CyclicBarrier。

CyclicBarrier

CyclicBarrier 是 Java 并发包 (java.util.concurrent) 中的一个同步辅助类,用于让一组线程彼此等待,直到所有线程都到达一个公共屏障点(barrier)。它适合于多线程协作的场景,例如分阶段计算或任务同步。

核心特性

  1. 可重用

    • 与CountDownLatch不同,CyclicBarrier在释放线程后可以被重置并重新使用。
  2. 屏障动作(Barrier Action)

    • 在所有线程到达屏障点时,可以指定一个额外的任务来执行,这个任务由一个线程完成。

主要方法

  1. CyclicBarrier(int parties)

    • 创建一个 CyclicBarrier,指定参与线程的数量。
  2. CyclicBarrier(int parties, Runnable barrierAction)

    • 创建一个 CyclicBarrier,指定参与线程的数量,并定义一个在所有线程到达屏障点时要执行的动作。
  3. int await()

    • 线程调用此方法等待其他线程到达屏障点。
    • 当所有线程都调用了 await(),屏障点打开,线程继续执行。
  4. int getNumberWaiting()

    • 返回当前有多少线程正在等待。
  5. int getParties()

    • 返回 CyclicBarrier 的参与线程总数。
  6. boolean isBroken()

    • 如果屏障点被打破(如线程被中断),返回 true。

工作原理

  1. 初始化时设置屏障线程数量(例如 new CyclicBarrier(3))。
  2. 每个线程调用 await() 方法后进入等待状态。
  3. 当所有线程到达屏障点时:

    • 可选:执行屏障动作(barrierAction)。
    • 屏障点打开,所有线程继续执行。
  4. 执行完成后,屏障会被重置,可以复用。

应用场景

  1. 分阶段任务
    多线程任务分为多个阶段,每个阶段需要所有线程同步后才能进入下一阶段。
  2. 并行计算
    将大任务分解为多个线程并行处理,所有线程完成后再汇总结果。
  3. 模拟比赛
    所有参赛选手准备好后一起开始比赛。

注意

  1. 复用机制
    CyclicBarrier 可以被复用,但只有当所有线程都通过屏障点后才能重置。
  2. 屏障损坏
    如果一个线程被中断或超时,CyclicBarrier 会进入损坏状态(BrokenBarrierException),其他线程也无法继续通过屏障。
  3. 屏障动作异常
    如果屏障动作(barrierAction)抛出异常,所有线程会抛出 BrokenBarrierException。

CyclicBarrier 与 CountDownLatch 的区别

特征 CyclicBarrier CountDownLatch
重用性 可重用,每次线程通过屏障后可以重置。 不可重用,计数器减到零后失效。
线程数量 需要所有线程都到达屏障点。 主线程可以等待多个线程完成任务。
屏障动作 支持屏障动作,线程同步后可执行操作。 不支持屏障动作。
典型场景 多阶段任务,分块并行计算。 任务初始化,主线程等待结果汇总。

Semaphore

Semaphore 是 Java 并发包(java.util.concurrent)中的一个同步工具类,用于控制同时访问特定资源的线程数量。它可以用于实现流量控制、资源池管理等场景。

核心特征

  1. 许可(Permit):信号量通过维护一个许可的计数来限制线程访问共享资源的数量。
  2. 可公平性(Fairness):支持公平模式,保证线程按照申请的顺序获取许可。
  3. 阻塞/非阻塞:支持线程获取许可时进行阻塞等待,或者尝试非阻塞获取许可。

构造方法

  1. Semaphore(int permits)
  2. 创建一个非公平的信号量,指定可用的许可数(permits)。
  3. Semaphore(int permits, boolean fair)
  4. 创建信号量,指定许可数和公平性:

    • true:公平模式,线程按申请顺序获取许可
    • false:非公平模式,可能发生插队

主要方法

  1. void acquire()

    • 从信号量中获取一个许可,若没有可用的许可,则阻塞等待。
  2. void acquire(int permits)

    • 获取指定数量的许可,若没有足够的许可,则阻塞等待。
  3. boolean tryAcquire()

    • 尝试获取一个许可,成功返回 true,失败返回 false,不阻塞。
  4. boolean tryAcquire(int permits, long timeout, TimeUnit unit)

    • 尝试在指定时间内获取许可,超时则返回 false。
  5. void release()

    • 释放一个许可,将其返回信号量。
  6. void release(int permits)

    • 释放指定数量的许可。
  7. int availablePermits()

    • 返回当前可用的许可数。
  8. int getQueueLength()

    • 返回正在等待许可的线程数。

代码实现

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        // 创建一个信号量,最多允许 3 个线程访问
        Semaphore semaphore = new Semaphore(3);

        // 创建并启动 6 个线程
        for (int i = 1; i <= 6; i++) {
            new Thread(new Task(semaphore, "Thread-" + i)).start();
        }
    }
}

class Task implements Runnable {
    private final Semaphore semaphore;
    private final String name;

    public Task(Semaphore semaphore, String name) {
        this.semaphore = semaphore;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + " is trying to acquire a permit...");
            semaphore.acquire(); // 获取许可
            System.out.println(name + " acquired a permit. Performing task...");
            Thread.sleep(2000); // 模拟任务执行
            System.out.println(name + " finished task. Releasing permit...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可
        }
    }
}

应用场景

  1. 资源池管理如:数据库连接池、线程池,限制最大并发资源访问数。
  2. 流量控制:限制访问某些资源(如 API 请求、文件写入)的速率。
  3. 任务限流:控制某一类任务的并发执行数量。

注意事项

  1. 非公平模式性能更高:

    • 非公平模式下,线程获取许可的效率更高,但可能导致某些线程“饥饿”。
  2. 许可释放需在 finally 块中

    • 确保发生异常时也能释放许可,避免信号量泄漏。
  3. 许可数量控制

    • 创建信号量时应合理设置许可数,过小可能导致任务阻塞过多。

Exchanger

Exchanger 是 Java 并发包 (java.util.concurrent) 中提供的一种线程同步工具,设计用于在两个线程之间交换数据。它可以让两个线程在一个同步点(同步屏障)相互交换数据,通常用于需要数据共享或双向通信的场景。

核心特性

  1. 一对一交换
    Exchanger 仅支持两个线程之间交换数据。
  2. 阻塞同步

    • 当一个线程调用 exchange() 时,会阻塞等待另一个线程到达同步点。
    • 两个线程都到达后,它们会交换数据并继续执行。
  3. 线程安全
    由 Exchanger 管理的交换过程是线程安全的。

构造方法

  • Exchanger<T>()
    创建一个可以交换类型为 T 的数据的 Exchanger 对象。

主要方法

  • T exchange(T data)
    当前线程将 data 传递给另一个线程,并接收另一个线程的 data。此方法会阻塞直到交换完成。
  • T exchange(T data, long timeout, TimeUnit unit)
    当前线程将 data 传递给另一个线程,并接收另一个线程的 data。如果在指定时间内未完成交换,会抛出 TimeoutException。

代码实现

import java.util.concurrent.Exchanger;

public class ExchangerExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread thread1 = new Thread(() -> {
            try {
                String data = "Data from Thread-1";
                System.out.println(Thread.currentThread().getName() + " is exchanging: " + data);
                String received = exchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + " received: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                String data = "Data from Thread-2";
                System.out.println(Thread.currentThread().getName() + " is exchanging: " + data);
                String received = exchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + " received: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        });

        thread1.start();
        thread2.start();
    }
}

应用场景

  1. 双缓冲优化
    一个线程生成数据,一个线程消费数据,通过 Exchanger 在两个线程之间切换缓冲区。
  2. 数据处理和验证
    两个线程处理同一数据的不同部分,并在同步点交换结果以整合最终输出。
  3. 任务分工与结果汇总
    两个线程各自执行不同的任务,并通过 Exchanger 共享计算结果。

注意事项

  1. 一对一线程配对

    Exchanger 只能用于两个线程,若只有一个线程调用 exchange(),它会一直阻塞。
  2. 超时控制

    使用带超时的 exchange() 避免线程无限期阻塞。
  3. 可能的死锁

    如果两个线程未能正确调用 exchange(),可能导致线程阻塞。例如,一个线程提前退出或抛出异常。
    

爱跑步的猕猴桃
1 声望0 粉丝