Java 多线程补充

时间:2024-05-11 08:50:46

线程池

Java线程池是一种能够有效管理线程资源的机制,它可以显著提高应用性能并降低资源消耗

线程池的主要优点包括:

  1. 资源利用高效:通过重用已存在的线程,减少了频繁创建和销毁线程带来的系统开销。
  2. 响应速度提升:任务到来时可以迅速被执行,而不必等待新线程的创建。
  3. 管理监控便捷:线程数量有限,避免无限制创建线程导致的资源耗尽和系统不稳定问题,同时便于统一分配、调优和监控。
  4. 功能丰富强大:提供了多种类型的线程池,如定时、定期以及可控线程数的线程池,满足不同的业务需求。

在使用线程池时,需要注意以下几点:

  1. 合理配置参数:创建线程池时需要根据实际需求合理设置线程数量等参数,以避免资源浪费或系统过载。
  2. 预防潜在风险:使用Executors类中的便捷方法虽然简单,但可能会隐藏复杂性,如不当使用可能导致内存溢出(OOM)或线程耗尽等问题。
  3. 选择正确任务类型:理解RunnableCallable接口的区别,并根据任务的特性选择合适的类型提交给线程池执行。

Executors

Java Executors是一个用于创建线程池的工厂类,它提供了一系列的静态工厂方法来简化线程池的创建和管理

Executors类中提供的方法包括:

  • newCachedThreadPool():创建一个可缓存的线程池,适用于执行大量的短期异步任务。线程数量可以根据需要自动扩展,如果有可用的空闲线程,就会重用它们;如果没有可用的线程,就会创建一个新线程。
  • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,适用于执行固定数量的长期任务。线程数量是固定的,不会自动扩展。
  • newSingleThreadExecutor():创建一个单线程的线程池,适用于需要按顺序执行任务的场景。
  • newScheduledThreadPool(int corePoolSize):创建一个固定大小的线程池,用于定时执行任务。线程数量固定,不会自动扩展。

使用Executors的优点包括:

  • 简化线程管理:Executors通过提供工厂方法,隐藏了线程池的复杂性,使得线程池的创建变得简单快捷。
  • 适应不同场景:根据不同的业务需求,可以选择不同类型的线程池,如固定大小、单线程或定时执行等。
  • 提高性能:通过复用线程,降低了资源消耗,提高了系统的响应速度和吞吐量。

以下是Java Executors的代码实现示例:

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

public class ExecutorsExample {
    public static void main(String[] args) {
        // 创建一个可缓存的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 提交任务到线程池
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Task());
        }

        // 关闭线程池
        executorService.shutdown();
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("Task executed by thread: " + Thread.currentThread().getName());
        }
    }
}

在这个例子中,我们使用Executors.newCachedThreadPool()方法创建了一个可缓存的线程池。然后,我们通过循环提交了10个任务到线程池中执行。每个任务都是一个实现了Runnable接口的Task类的实例。最后,我们调用executorService.shutdown()方法关闭线程池。

 ThreadLocal

Java中的ThreadLocal是一个用于存储线程局部变量的类,它允许每个线程拥有自己的独立变量副本,从而实现线程间的数据隔离

ThreadLocal的主要作用是解决多线程环境下的数据安全问题,避免多个线程同时访问共享变量时出现数据不一致的情况。通过使用ThreadLocal,每个线程都可以在本地存储自己的私有数据,而不会影响其他线程的数据。

以下是ThreadLocal的一些主要特点:

  1. 线程安全:ThreadLocal为每个线程提供了独立的变量副本,避免了多线程之间的数据竞争和同步问题。
  2. 高效性:相比于使用同步机制来保护共享变量,ThreadLocal可以提供更高的性能,因为它避免了锁的使用和线程阻塞。
  3. 内存泄漏风险:由于ThreadLocal的生命周期与线程相同,如果不及时清理ThreadLocal中的数据,可能会导致内存泄漏。因此,在使用完ThreadLocal后,需要手动调用remove()方法来清除数据。
  4. 可扩展性:ThreadLocal可以通过继承或实现自定义的ThreadLocal子类来扩展其功能,以满足特定的业务需求。
  5. 适用场景:ThreadLocal适用于需要在多线程环境下保持线程间数据隔离的场景,例如数据库连接池、会话管理等。

下面是一个简单的示例代码,演示了如何使用ThreadLocal来存储和获取线程局部变量:

public class ThreadLocalExample {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 设置线程局部变量的值
        threadLocal.set("Hello, World!");

        // 获取线程局部变量的值
        String value = threadLocal.get();
        System.out.println(value); // 输出: Hello, World!

        // 清除线程局部变量的值
        threadLocal.remove();
    }
}

在这个例子中,我们创建了一个名为threadLocal的ThreadLocal对象,并使用set()方法设置了线程局部变量的值。然后,我们使用get()方法获取了该值,并将其打印出来。最后,我们调用remove()方法清除了线程局部变量的值,以避免内存泄漏。

原子性

Java中的原子性是指一个操作或者一系列操作要么全部执行成功,要么全部失败,且在执行过程中不会被其他线程打断

原子性是并发编程中的一个重要概念,它保证了线程安全,即在一个线程执行操作时,不受其他线程的干扰。以下是保证原子性的几种方法:

  1. 使用synchronized关键字:通过同步代码块或同步方法来确保在同一时刻只有一个线程能够访问共享资源。
  2. 使用volatile关键字:虽然volatile不能保证复合操作的原子性,但它可以保证单个共享变量的读写操作是原子性的,并且能够保证变量的可见性。
  3. 使用Atomic类:Java提供了一系列的Atomic类(如AtomicInteger、AtomicLong等),它们使用非阻塞算法来实现对单个变量的原子操作。
  4. 使用Lock接口及其实现类:如ReentrantLock,它们提供了比synchronized更灵活的锁定机制,可以控制锁的获取和释放。
  5. 使用CAS操作:比较并交换(Compare-and-Swap)是一种无锁技术,用于实现高效的并发控制。
  6. 使用线程安全的数据结构:如ConcurrentHashMap、CopyOnWriteArrayList等,这些数据结构内部实现了必要的同步措施,以保证并发访问时的线程安全。
  7. 使用信号量、倒计时门闩等同步辅助工具:这些工具可以帮助控制并发线程的执行顺序和数量,从而保证操作的原子性。

以下是Java中保证原子性的代码示例:

  • 使用synchronized关键字保证原子性:
public class AtomicityExample {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}
  • 使用volatile关键字保证单个共享变量的原子性:
public class AtomicityExample {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
  • 使用AtomicInteger保证单个共享变量的原子性:
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicityExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}
  • 使用Lock接口及其实现类ReentrantLock保证原子性:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

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

    public int getCount() {
        return count;
    }
}
  • 使用CAS操作保证原子性:
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicityExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        int oldValue, newValue;
        do {
            oldValue = count.get();
            newValue = oldValue + 1;
        } while (!count.compareAndSet(oldValue, newValue));
    }

    public int getCount() {
        return count.get();
    }
}

并发工具类-Hashtable

Hashtable是Java集合框架中的一部分,它实现了Map接口,并且支持同步

Hashtable在Java中是一个较早出现的集合类,它提供了一种存储键值对的方式,这些键值对被称为条目,它们存储在一个哈希表中。以下是关于Hashtable的一些详细信息:

  • 实现和接口:Hashtable是java.util包的一部分,并且它是Dictionary接口的具体实现。自Java 2以来,Hashtable还实现了Map接口,这意味着它可以被当作一个映射来使用。
  • 线程安全:与HashMap不同,Hashtable是线程安全的,因为它支持同步。这允许多个线程同时访问Hashtable而不会发生并发问题。
  • 存储方式:Hashtable通过计算对象的哈希码来确定其在内部数组中的存储位置。当两个对象具有相同的哈希码时,它们会被存储在同一个索引位置,形成一个链表。
  • 基本操作:Hashtable提供了常见的操作方法,如put()用于添加或更新键值对,get()用于根据键获取值,remove()用于删除键值对等。
  • 性能考虑:由于Hashtable的同步特性,它在单线程环境下的性能可能不如非同步的HashMap。在高并发场景下,如果对性能有较高要求,可以考虑使用ConcurrentHashMap作为替代方案。
  • 使用示例:Hashtable的使用非常简单,可以像下面这样创建一个Hashtable实例,并向其中添加元素:
Hashtable<Integer, String> hashtable = new Hashtable<>();
hashtable.put(1, "aa");
hashtable.put(4, "dd");
hashtable.put(2, "bb");
hashtable.put(3, "cc");
System.out.println(hashtable);

需要注意的是,虽然Hashtable是线程安全的,但在迭代时仍需手动同步以保证一致性。此外,Hashtable不允许键或值为null,这一点在使用时应特别注意。

并发工具类-CountDownLatch

CountDownLatch是Java并发编程中的一个同步辅助类,它允许一个或多个线程等待直到其他线程完成操作。

以下是CountDownLatch的一些主要特点和使用场景:

基本概念:CountDownLatch通过一个计数器来实现线程之间的同步。计数器的初始值由用户设置,每次调用countDown()方法会使计数器的值减一,当计数器的值减至零时,所有因调用await()方法而在等待的线程被唤醒。

应用场景:CountDownLatch通常用于以下场景:

  • 等待其他线程完成任务后再执行:例如,主线程需要等待其他线程完成初始化操作后才能继续执行。
  • 实现多个线程之间的同步:例如,确保所有线程都准备好后再同时开始执行。

使用示例

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threads = 5;
        CountDownLatch latch = new CountDownLatch(threads);

        for (int i = 0; i < threads; i++) {
            new Thread(new Worker(latch)).start();
        }

        latch.await(); // 主线程等待其他线程完成任务
        System.out.println("所有线程任务完成,主线程继续执行...");
    }
}

class Worker implements Runnable {
    private final CountDownLatch latch;

    Worker(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            // 模拟耗时操作
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " 任务完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            latch.countDown(); // 完成任务后,计数器减一
        }
    }
}

注意事项

  • CountDownLatch不能重置计数器,一旦计数器减至零,就不能再被使用。如果需要重复使用,需要创建新的CountDownLatch实例。
  • 在调用countDown()await()方法时可能会抛出InterruptedException,因此需要进行适当的异常处理

并发工具类-Semaphore

Java Semaphore是Java并发编程中的一个同步辅助类,它允许多个线程访问同一资源,但限制同时访问的线程数量。

以下是Semaphore的一些主要特点和使用场景:

基本概念:Semaphore通过一个计数器来实现线程之间的同步。计数器的初始值由用户设置,每次调用acquire()方法会使计数器的值减一,当计数器的值减至零时,其他尝试获取许可的线程将被阻塞。当线程释放许可时,调用release()方法会使计数器的值加一。

应用场景:Semaphore通常用于以下场景:

  • 限制同时访问资源的线程数量:例如,限制同时访问数据库连接的线程数量,以避免过多的连接导致系统负载过高。
  • 实现信号量机制:例如,控制生产者和消费者之间的生产消费速度,确保生产者不会过度生产而使缓冲区溢出。

使用示例

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permits = 5; // 允许同时访问资源的线程数量
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 0; i < 10; i++) {
            new Thread(new Worker(semaphore)).start();
        }
    }
}

class Worker implements Runnable {
    private final Semaphore semaphore;

    Worker(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire(); // 获取许可
            // 执行任务
            System.out.println(Thread.currentThread().getName() + " 正在执行任务");
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可
        }
    }
}

注意事项

  • Semaphore不能重置计数器,一旦计数器减至零,就不能再被使用。如果需要重复使用,需要创建新的Semaphore实例。
  • 在调用acquire()release()方法时可能会抛出InterruptedException,因此需要进行适当的异常处理。