Java 多线程系列Ⅶ

2024-02-27 09:08:18 浏览数 (2)

前言

JUC 是 Java 并发包(java.util.concurrent)的简称,该包在 Java 5.0 版本引入,提供了在并发编程中常用的工具类。这些工具类包括用于多线程同步的锁(如 ReentrantLock),用于线程池的 ExecutorService,用于原子操作的 AtomicInteger 等。这些类和接口都是非阻塞的,设计目标是在多线程环境下提供高性能和可扩展性。

JUC 包中的类和接口主要分为三类:

  1. 同步工具类:例如 Lock、Semaphore、CountDownLatch、CyclicBarrier、Exchanger 等,这些类提供了多种同步机制,可以灵活地控制多个线程之间的执行顺序和协作。
  2. 线程池类:例如 ThreadPoolExecutor、ScheduledThreadPoolExecutor 等,这些类可以方便地创建和管理线程池,有效地利用系统资源,避免线程过多或者过少的问题。
  3. 原子操作类:例如 AtomicInteger、AtomicLong、AtomicReference 等,这些类提供了原子操作,可以在多线程环境下保证数据的一致性和安全性。

一、Callable

1、介绍

Callable接口是Java中的一个泛型接口,它允许在并发环境中定义可以返回结果的任务。与Runnable接口不同的是,Callable接口可以返回执行结果,并且可以抛出异常。

2、Callable的使用

使用Callable接口需要实现Callable接口,并重写call()方法。call()方法定义了要执行的任务,并返回执行结果。

代码语言:javascript复制
public class MyCallable implements Callable<Integer> {  
    @Override  
    public Integer call() throws Exception {  
        // 执行任务,并返回结果  
        return 42;  
    }  
}

在上述代码中,MyCallable实现了Callable接口,并返回一个Integer类型的结果。call()方法定义了要执行的任务,并返回执行结果。

3、具体例子

使用线程池ExecutorService执行一个Callable任务,并获取返回结果:

代码语言:javascript复制
import java.util.concurrent.*;  
  
public class Example {  
    public static void main(String[] args) {  
        ExecutorService executor = Executors.newFixedThreadPool(1);  
        Callable<Integer> task = new MyCallable();  
        Future<Integer> future = executor.submit(task);  
        try {  
            Integer result = future.get(); // 获取返回结果  
            System.out.println("Result: "   result);  
        } catch (InterruptedException | ExecutionException e) {  
            e.printStackTrace();  
        } finally {  
            executor.shutdown();  
        }  
    }  
}

例子解析:Example创建了一个线程池ExecutorService,并提交了一个MyCallable任务。通过调用Future的get()方法获取任务的返回结果。如果任务执行成功,则返回结果;如果任务执行失败,则抛出ExecutionException异常。最后,Example关闭线程池。

4、Callable与Runnable比较

  1. Callable接口可以返回执行结果,而Runnable接口不能。
  2. Callable接口可以抛出异常,而Runnable接口不能。
  3. Callable接口的实现类需要实现call()方法,而Runnable接口的实现类需要实现run()方法。
  4. Runnable接口通常用于实现简单的后台任务,而Callable接口通常用于需要返回结果的复杂任务。
  5. Runnable和Callable的设计目标不同:Runnable设计目标是在多线程环境下提供简单的执行代码的机制;Callable设计目标是在多线程环境下提供具有返回值的任务执行机制。
  6. 在使用线程池时,通常使用Callable接口,因为线程池可以更好地管理线程资源,并允许在任务完成后自动关闭线程。而Runnable通常直接提交给Thread对象来执行。
  7. Callable可以在任务执行前进行初始化操作;而Runnable没有这样的机制。比如:可以在任务执行前对局部变量进行初始化;或者在任务执行前进行资源的预分配等。这种特性在某些场景下非常有用。比如:一个任务需要大量计算和内存消耗,为了减少垃圾回收的频率,可以在任务执行前进行内存的大量分配。但是请注意:大量使用这种方式可能会导致内存的浪费和内存溢出等问题。因此在使用时需要谨慎考虑其性能和内存消耗问题。

当然Callable接口主要是创建线程的新方式,目前我们学过的创建线程的方式总结如下: 1、继承Thread类 2、实现Runnable接口 3、实现Callable接口 4、线程池

二、ReentrantLock

1、介绍

ReentrantLock是一种可重入锁,它允许一个线程多次获取同一个锁,而不会产生死锁。ReentrantLock在加锁时采用了公平锁策略,即等待时间最长的线程将获得锁。与内置的synchronized锁不同,ReentrantLock是非阻塞的,它允许线程在等待锁的过程中执行其他任务。

2、ReentrantLock的使用

通常,我们使用ReentrantLock时需要先创建一个ReentrantLock对象,然后在需要加锁的地方调用lock()方法获取锁,执行任务后调用unlock()方法释放锁。

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;  
  
public class MyClass {  
    private final ReentrantLock lock = new ReentrantLock();  
  
    public void doSomething() {  
        lock.lock(); // 加锁  
        try {  
            // 执行任务  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}

代码解析:MyClass创建了一个ReentrantLock对象,并在doSomething()方法中进行了加锁和释放锁的操作。使用try-finally语句块确保在任务执行完毕后释放锁。

3、具体例子

可以使用ReentrantLock实现线程安全的计数器:

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;  
  
public class Counter {  
    private int count = 0;  
    private final ReentrantLock lock = new ReentrantLock();  
  
    public void increment() {  
        lock.lock(); // 加锁  
        try {  
            count  ;  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
  
    public int getCount() {  
        return count;  
    }  
}

通常,我们使用的计数器只支持我们在单线程下使用,如果想要在多线程下使用,通常需要加锁,Counter创建了一个ReentrantLock对象和一个计数器变量count。increment()方法用于对计数器进行加1操作,并使用ReentrantLock进行同步。getCount()方法返回计数器的当前值。由于使用了ReentrantLock,这个计数器可以在多线程环境下安全地使用。

4、ReentrantLock和Synchronized的区别

1、底层实现:synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

2、实现原理:synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

3、是否可手动释放: synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象

4、是否可中断synchronized是不可中断类型的锁,除非加锁的代码中出现异常或非正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

5、是否公平锁synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。

三、Atomic 原子类

1、介绍

在多线程环境下,多个线程可能会同时访问和修改同一个共享变量,这时就需要一种机制来保证操作的原子性,以避免出现竞争条件和数据不一致的问题。Java中的Atomic原子类提供了一种高效的方式来解决这个问题。Atomic原子类通过使用CPU的原子指令来保证操作的原子性,避免了使用锁的开销和竞争。

2、Atomic 使用

代码语言:javascript复制
import java.util.concurrent.atomic.AtomicInteger;  
  
public class MyApp {  
    private static AtomicInteger counter = new AtomicInteger(0);  
  
    public static void main(String[] args) {  
        // 增加计数器值  
        counter.incrementAndGet();  
        // 获取计数器值  
        int value = counter.get();  
        System.out.println("Counter value: "   value);  
    }  
}

3、具体例子

也是和上面一样实现一个线程安全的计数器:

代码语言:javascript复制
import java.util.concurrent.atomic.AtomicLong;  
  
public class AtomicCounter {  
    private AtomicLong counter = new AtomicLong(0);  
  
    public void increment() {  
        counter.incrementAndGet();  
    }  
  
    public long get() {  
        return counter.get();  
    }  
}

代码解析:我们创建了一个AtomicLong对象counter,并使用incrementAndGet()方法增加计数器的值。然后使用get()方法获取计数器的值。由于AtomicLong提供了原子操作,因此即使多个线程同时访问和修改计数器,也不会出现竞争条件和数据不一致的问题。

4、Atomic特点

  1. 原子性:Atomic原子类提供了一种在多线程环境下进行原子操作的方式,保证了操作的原子性。
  2. 非阻塞性:Atomic原子类避免了使用锁的开销和竞争,从而提高了程序的响应能力和吞吐量。
  3. 高效性:Atomic原子类通过使用CPU的原子指令来保证操作的原子性,避免了出现竞争条件和数据不一致的问题。
  4. 可移植性:Atomic原子类的实现与Java虚拟机规范保持一致,因此可以在不同的Java虚拟机上使用。

0 人点赞