史上最详细Java面试总结—并发编程篇,由浅入深带你深入了解并发编程

1. 什么是线程?Java中如何创建线程?

线程的定义和特点。

线程的定义:

线程是计算机中的基本执行单元,是进程中的一个独立执行流。一个进程可以包含多个线程,这些线程共享进程的资源,但每个线程有自己的执行路径、局部变量和栈。线程允许程序同时执行多个任务,提高了程序的并发性。

线程的特点:

  1. 轻量级: 相对于进程而言,线程是轻量级的执行单元。线程不拥有系统资源,只有一点必不可少的资源,如程序计数器、一组寄存器和栈。

  2. 共享进程资源: 线程属于同一进程,它们共享进程的内存空间和资源。这使得线程之间的通信更加方便,但也需要进行合理的同步操作以防止竞态条件。

  3. 独立执行流: 每个线程都有自己的执行路径,线程在执行过程中独立运行,互不干扰。但为了协同工作,线程之间可能需要同步机制来协调执行顺序。

  4. 上下文切换: 在多线程环境下,CPU会不断地切换执行不同的线程,这就是上下文切换。上下文切换是有开销的,需要保存和恢复寄存器、程序计数器等状态。

  5. 并发性: 多线程的主要优势在于提高程序的并发性,充分利用CPU资源,实现多个任务同时执行,从而提高系统的整体性能。

  6. 局部变量和栈: 每个线程都有自己的局部变量和栈,局部变量对于线程是私有的,不会被其他线程访问。栈用于存储线程的方法调用和局部变量,是线程私有的。

  7. 线程之间的通信: 线程之间可以通过共享内存进行通信,也可以使用专门的通信机制,如wait、notify、notifyAll等,实现线程之间的协作。

  8. 生命周期: 线程有不同的生命周期,包括新建、就绪、运行、阻塞、终止等状态。线程的状态转换由系统和线程本身的操作共同决定。

总体而言,线程是实现并发编程的基本单元,具有轻量级、共享资源、独立执行流等特点,通过合理的同步和通信机制,可以实现多任务的协同工作。

Thread类和Runnable接口的关系。

在Java中,Thread类和Runnable接口是用于创建和管理多线程的两种主要方式。它们之间的关系是Thread类可以实现Runnable接口。以下是它们之间的关系和使用方式:

  1. Thread类:
    • Thread类是Java中用于表示线程的类,它直接继承自Object类,并实现了Runnable接口。
    • Thread类本身具有创建和启动线程的能力。你可以通过继承Thread类,覆写它的run()方法来定义线程的执行逻辑。
    • Thread类也提供了其他一些用于管理线程的方法,如start()方法用于启动线程,sleep()方法用于让线程休眠一段时间等。
class MyThread extends Thread {
   
    public void run() {
   
        // 线程执行的逻辑
    }
}
  1. Runnable接口:
    • Runnable接口是一个功能接口,它只包含一个抽象方法run(),用于定义线程的执行逻辑。
    • 通过实现Runnable接口,可以将线程的任务与线程的实现解耦,提高代码的灵活性。多个线程可以共享同一个Runnable实例,实现了资源的共享。
class MyRunnable implements Runnable {
   
    public void run() {
   
        // 线程执行的逻辑
    }
}
  1. Thread类实现Runnable接口的方式:
    • Thread类本身就实现了Runnable接口。如果你希望使用Thread类,并且同时希望定义线程的执行逻辑,可以通过重写run()方法来实现。
class MyThread extends Thread {
   
    public void run() {
   
        // 线程执行的逻辑
    }
}
  1. 创建和启动线程的方式:
    • 使用Thread类的方式:
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
  • 使用Runnable接口的方式:
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程

通过实现Runnable接口,你可以更灵活地组织代码,因为Java中支持多重继承,而一个类只能继承一个类。因此,如果你希望继承其他类,同时又需要实现线程的功能,就可以使用Runnable接口。一般来说,推荐使用实现Runnable接口的方式,因为它更灵活,可以避免Java单继承的限制。

使用Thread和Runnable创建线程的步骤。

创建线程的方式主要有两种:使用Thread类和使用Runnable接口。下面分别介绍使用这两种方式创建线程的步骤:

使用Thread类创建线程的步骤:

  1. 创建一个继承自Thread的子类:

    • 创建一个新的类,继承自Thread类。重写该类的run()方法,该方法包含线程的执行逻辑。
    class MyThread extends Thread {
         
        public void run() {
         
            // 线程执行的逻辑
        }
    }
    
  2. 创建线程对象:

    • 在主程序中创建MyThread类的实例。
    MyThread myThread = new MyThread();
    
  3. 启动线程:

    • 调用线程对象的start()方法来启动线程。start()方法会自动调用run()方法,使得线程执行其逻辑。
    myThread.start();
    

使用Runnable接口创建线程的步骤:

  1. 创建一个实现了Runnable接口的类:

    • 创建一个新的类,实现Runnable接口。实现Runnable接口需要定义run()方法,该方法包含线程的执行逻辑。
    class MyRunnable implements Runnable {
         
        public void run() {
         
            // 线程执行的逻辑
        }
    }
    
  2. 创建线程对象:

    • 在主程序中创建MyRunnable类的实例,并传递给Thread类的构造方法。
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    
  3. 启动线程:

    • 调用Thread对象的start()方法来启动线程。
    thread.start();
    

通过上述步骤,你可以使用Thread类或Runnable接口创建并启动线程。使用Runnable接口的方式更灵活,因为一个类可以同时实现多个接口,而继承Thread类则不能再继承其他类。根据具体的需求和设计考虑,选择合适的方式创建线程。

通过Executor框架创建线程。

Java中的Executor框架提供了一种更高层次、更灵活的方式来管理线程池。通过Executor框架,可以避免手动管理线程的创建和销毁,提高了代码的可读性和维护性。下面是通过Executor框架创建线程的步骤:

  1. 创建Runnable任务或Callable任务:

    • 首先,你需要创建一个实现了Runnable接口或Callable接口的任务。Runnable用于无返回值的任务,而Callable用于具有返回值的任务。
    class MyRunnable implements Runnable {
         
        public void run() {
         
            // 线程执行的逻辑
        }
    }
    
  2. 创建ExecutorService:

    • 使用Executors工厂类创建一个ExecutorService对象,该对象代表线程池。
    ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
    

    这里创建了一个固定大小的线程池,可以根据实际需求选择不同类型的线程池,例如newCachedThreadPool()(缓存线程池)或newSingleThreadExecutor()(单线程线程池)等。

  3. 提交任务给线程池:

    • 使用ExecutorService的submit()方法提交任务给线程池。
    MyRunnable myRunnable = new MyRunnable();
    executorService.submit(myRunnable);
    

    这样,线程池会负责调度任务的执行,而无需手动创建和管理线程。

  4. 关闭线程池:

    • 在程序结束时,需要手动关闭线程池,释放资源。这是一种良好的实践,可以确保程序正常退出。
    executorService.shutdown();
    

    你也可以使用shutdownNow()方法,它会尝试停止所有正在执行的任务,不再接受新的任务。

通过Executor框架,你可以轻松地管理线程池,提高了代码的可维护性和可扩展性。此外,Executor框架还提供了一些高级功能,例如定时执行任务、管理任务的完成等,使得线程池的使用更加灵活。

2. 解释Java中的同步和异步的概念。

同步和异步的定义和区别。

同步和异步的定义和区别:

  1. 同步(Synchronous):

    • 定义: 同步是指按照顺序执行任务,一个任务的执行会阻塞后续任务的开始,直到前一个任务完成。
    • 特点: 同步操作是按顺序执行的,一个任务完成后,才能开始下一个任务。同步操作通常是阻塞的,任务执行的结果需要等待,直到获取结果后才能继续执行下一步操作。
  2. 异步(Asynchronous):

    • 定义: 异步是指不按照顺序执行任务,一个任务的执行不会阻塞后续任务的开始,可以在任务执行的过程中继续执行其他任务。
    • 特点: 异步操作允许多个任务并行执行,不需要等待前一个任务完成。任务执行的结果通常通过回调、事件等方式来处理,而不是通过直接等待。

区别:

  • 执行顺序:

    • 同步: 按照顺序执行,一个任务完成后才能开始下一个任务。
    • 异步: 不按照顺序执行,任务的执行不阻塞后续任务的开始。
  • 阻塞与非阻塞:

    • 同步: 同步操作通常是阻塞的,任务执行的结果需要等待。
    • 异步: 异步操作是非阻塞的,可以在任务执行的过程中继续执行其他任务。
  • 任务完成通知:

    • 同步: 任务完成后,直接得到结果,继续执行下一步操作。
    • 异步: 任务完成后,通常通过回调、事件等方式来处理结果。
  • 例子:

    • 同步: 传统的函数调用就是同步的,调用者需要等待被调用函数执行完毕才能继续执行。
    • 异步: 异步的例子包括JavaScript中的异步回调、Java中的Future和CompletableFuture等。

在实际编程中,选择同步还是异步取决于具体的场景和需求。同步适用于简单、顺序的任务,而异步适用于需要提高程序并发性和响应性的场景,如处理大量IO操作、网络请求等。

在Java中如何实现同步和异步操作。

在Java中,同步和异步操作可以通过不同的机制来实现。以下是一些常见的方法:

同步操作:

  1. 使用关键字 synchronized:

    • 在Java中,可以使用关键字 synchronized 来实现同步。通过将关键代码块或方法声明为 synchronized,可以确保同时只有一个线程访问它们。
    public synchronized void synchronizedMethod() {
         
        // 同步的代码块
    }
    
  2. 使用 Lock 接口:

    • Java中的 Lock 接口提供了更灵活的锁定机制。通过 ReentrantLock 或其他实现类,可以在代码块中手动加锁和解锁。
    ReentrantLock lock = new ReentrantLock();
    
    public void synchronizedMethod() {
         
        lock.lock();
        try {
         
            // 同步的代码块
        } finally {
         
            lock.unlock();
        }
    }
    

异步操作:

  1. 使用回调函数:

    • 在异步编程中,经常使用回调函数的方式处理异步结果。定义一个回调接口,将需要异步执行的操作通过回调函数传递给异步任务。
    interface AsyncCallback {
         
        void onComplete(String result);
    }
    
    public void asyncOperation(AsyncCallback callback) {
         
        // 异步操作完成后调用回调函数
        String result = performAsyncOperation();
        callback.onComplete(result);
    }
    
  2. 使用 Future 和 CompletableFuture:

    • Java中的 FutureCompletableFuture 类提供了处理异步任务的机制。通过 CompletableFuture,可以使用 thenApplythenCompose 等方法链式地处理异步操作。
    CompletableFuture.supplyAsync(() -> performAsyncOperation())
                     .thenApply(result -> processResult(result))
                     .thenAccept(finalResult -> handleFinalResult(finalResult));
    
  3. 使用异步框架:

    • Java中的一些框架(例如,Spring的 @Async、Guava的 ListenableFuture)提供了更高级别的异步处理机制,允许在代码中直接声明异步方法或任务。
    @Async
    public void asyncMethod() {
         
        // 异步的代码逻辑
    }
    

选择同步还是异步的方式取决于具体的应用场景和需求。在处理 IO 操作、网络请求等需要等待的情况下,使用异步操作可以提高程序的并发性和响应性。而对于简单的顺序执行任务,同步操作通常更加直观和易于理解。

举例说明同步和异步的应用场景。

同步的应用场景:

  1. 图形用户界面(GUI)操作:

    • 在图形用户界面中,用户的交互操作通常需要同步处理,以确保用户界面的响应性。例如,按钮点击、文本框输入等用户操作都需要在主线程中同步处理。
    button.addActionListener(e -> {
         
        // 同步处理按钮点击事件
    });
    
  2. 文件读写操作:

    • 在文件读写操作中,为了确保数据的一致性和正确性,通常需要使用同步机制。例如,使用 synchronized 关键字或锁来保护共享资源。
    public synchronized void writeFile(String content) {
         
        // 同步写文件的操作
    }
    
  3. 数据库事务:

    • 在数据库操作中,涉及事务的操作通常需要同步处理,以确保事务的原子性。数据库的读写操作需要在同一事务中进行。
    try {
         
        // 同步处理数据库事务
        connection.setAutoCommit(false);
        // 执行数据库操作
        connection.commit();
    } catch (SQLException e) {
         
        connection.rollback();
    } finally {
         
        connection.setAutoCommit(true);
    }
    

异步的应用场景:

  1. 网络请求:

    • 在进行网络请求时,通常使用异步操作来避免阻塞主线程。这可以确保用户界面保持响应性,而不会因为等待网络响应而被冻结。
    CompletableFuture.supplyAsync(() -> performNetworkRequest())
                     .thenAccept(response -> handleResponse(response));
    
  2. 多线程计算:

    • 在进行密集计算的场景中,使用异步操作可以提高程序的并发性,充分利用多核处理器。通过使用线程池等机制,可以异步执行多个计算任务。
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    List<Future<Integer>> futures = new ArrayList<>();
    
    for (int i = 0; i < 4; i++) {
         
        Future<Integer> future = executorService.submit(new CalculationTask());
        futures.add(future);
    }
    
    // 异步获取计算结果
    for (Future<Integer> future : futures) {
         
        Integer result = future.get();
        // 处理计算结果
    }
    
    executorService.shutdown();
    
  3. 事件驱动编程:

    • 在事件驱动编程中,通常使用异步机制处理事件回调。例如,用户点击按钮时触发的事件处理可以异步执行,而不阻塞用户界面。
    button.addActionListener(e -> {
         
        CompletableFuture.runAsync(() -> {
         
            // 异步处理按钮点击事件
        });
    });
    

选择同步还是异步的方式取决于具体的需求和性能优化目标。同步适用于一些需要按照顺序执行的场景,而异步适用于提高程序并发性和响应性的情况。

3. 什么是线程安全?如何确保线程安全?

线程安全的定义。

线程安全的定义:

线程安全(Thread Safety)是指在多线程环境中,一个类、对象或方法在被多个线程同时访问时仍能保持其正确性、稳定性和一致性,而不出现数据不一致或意外结果的情况。一个线程安全的操作不会因为其他线程的并发操作而导致数据的损坏或不一致。

线程安全的实现通常需要考虑多个线程之间的竞态条件、共享资源的访问、数据的原子性、并发控制等问题。在多线程编程中,保证线程安全是确保程序正确运行的关键因素之一。

线程安全的关键要点包括:

  1. 原子性(Atomicity):

    • 单个操作是原子的,即它们在执行过程中不会被其他线程中断,要么全部执行成功,要么全部失败。在多线程环境中,可以使用锁、CAS(Compare and Swap)等机制来保证原子性操作。
  2. 可见性(Visibility):

    • 当一个线程修改了共享变量的值时,其他线程能够立即看到这个变化。这可以通过使用volatile关键字、锁、同步块等来实现。
  3. 有序性(Ordering):

    • 指令在多线程环境中的执行顺序应该符合程序的顺序,防止指令重排序导致的错误。通过使用volatile关键字、锁、同步块等可以保证指令的有序性。
  4. 竞态条件(Race Condition):

    • 多个线程同时访问共享资源,如果没有适当的同步措施,可能导致数据不一致。使用锁、同步块、原子变量等来避免竞态条件。
  5. 死锁(Deadlock):

    • 多个线程相互等待对方释放锁,导致程序无法继续执行。使用合理的锁顺序、锁超时等策略来预防死锁。

线程安全是多线程编程中的一个复杂问题,需要仔细的设计和考虑。在Java中,可以使用synchronized关键字、ReentrantLockvolatile关键字等来实现线程安全,也可以使用并发集合类(如ConcurrentHashMapCopyOnWriteArrayList等)来简化线程安全的实现。

常见的线程安全和非线程安全的集合类。

常见的线程安全集合类:

  1. ConcurrentHashMap

    • ConcurrentHashMap 是线程安全的哈希表实现,支持高并发的读和写操作,通过分段锁(Segment)来实现并发控制。
    Map<String, String> concurrentMap = new ConcurrentHashMap<>();
    
  2. CopyOnWriteArrayList

    • CopyOnWriteArrayList 是线程安全的动态数组实现,适用于读多写少的场景。写操作时会复制一份新的数组,因此不会影响正在进行的读操作。
    List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
    
  3. CopyOnWriteArraySet

    • CopyOnWriteArraySet 是线程安全的集合,是基于 CopyOnWriteArrayList 实现的。适用于读多写少的场景。
    Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>();
    
  4. BlockingQueue 接口的实现类(例如 LinkedBlockingQueue):

    • BlockingQueue 接口及其实现类是用于多线程间安全地传递数据的队列。LinkedBlockingQueue 是其中的一种,支持阻塞操作。
    BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
    

非线程安全的集合类:

  1. ArrayList

    • ArrayList 是非线程安全的动态数组实现。在多线程环境中,如果没有适当的同步措施,对 ArrayList 的并发操作可能会导致不一致的结果。
    List<String> arrayList = new ArrayList<>();
    
  2. HashMap

    • HashMap 是非线程安全的哈希表实现。在多线程环境中,如果没有适当的同步措施,对 HashMap 的并发操作可能会导致不一致的结果。
    Map<String, String> hashMap = new HashMap<>();
    
  3. HashSet

    • HashSet 是非线程安全的集合。在多线程环境中,如果没有适当的同步措施,对 HashSet 的并发操作可能会导致不一致的结果。
    Set<String> hashSet = new HashSet<>();
    
  4. Hashtable

    • Hashtable 是一个旧的哈希表实现,虽然它是线程安全的,但因为其性能较差,不推荐在新代码中使用。建议使用 ConcurrentHashMap 替代。
    Hashtable<String, String> hashtable = new Hashtable<>();
    

在多线程环境中,选择适当的线程安全集合类是至关重要的。线程安全的集合类能够有效地避免并发问题,而非线程安全的集合类则需要额外的同步措施来保证线程安全。

使用synchronized和ReentrantLock实现线程安全。

在Java中,可以使用 synchronized 关键字和 ReentrantLock 类来实现线程安全。下面分别展示如何使用这两种方式来保证多线程环境下的线程安全:

使用 synchronized 关键字:

public class SynchronizedExample {
   
    private int count = 0;

    // 使用 synchronized 关键字保证线程安全
    public synchronized void increment() {
   
        count++;
    }

    public int getCount() {
   
        return count;
    }

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

        // 创建多个线程并发调用 increment 方法
        Runnable task = () -> {
   
            for (int i = 0; i < 1000; i++) {
   
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        try {
   
            // 等待两个线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }

        // 输出结果
        System.out.println("Count: " + example.getCount()); // Count: 2000
    }
}

使用 ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    // 使用 ReentrantLock 保证线程安全
    public void increment() {
   
        lock.lock();
        try {
   
            count++;
        } finally {
   
            lock.unlock();
        }
    }

    public int getCount() {
   
        return count;
    }

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

        // 创建多个线程并发调用 increment 方法
        Runnable task = () -> {
   
            for (int i = 0; i < 1000; i++) {
   
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        try {
   
            // 等待两个线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }

        // 输出结果
        System.out.println("Count: " + example.getCount()); // Count: 2000
    }
}

在上述例子中,SynchronizedExample 使用了 synchronized 关键字,而 ReentrantLockExample 使用了 ReentrantLock。两者都能够有效地保证 increment 方法的线程安全性。选择使用哪种方式通常取决于具体的需求,ReentrantLock 提供了更灵活的锁定机制,但相对而言使用起来稍显繁琐。

使用volatile关键字的线程安全性。

volatile 关键字是一种用于修饰实例变量的轻量级同步机制,主要用于保证该变量在多线程环境下的可见性和禁止指令重排序。volatile 可以保证一个线程对共享变量的修改能够被其他线程及时感知,但它并不能保证原子性。

下面是一个使用 volatile 关键字的例子,演示了如何通过 volatile 保证变量在多线程环境中的可见性:

public class VolatileExample {
   
    private volatile boolean flag = false;

    public void setFlag() {
   
        flag = true;
    }

    public void printFlag() {
   
        while (!flag) {
   
            // 等待 flag 变为 true
        }
        System.out.println("Flag is now true!");
    }

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

        // 线程1设置 flag 为 true
        new Thread(() -> {
   
            example.setFlag();
        }).start();

        // 线程2等待 flag 变为 true
        new Thread(() -> {
   
            example.printFlag();
        }).start();
    }
}

在上述例子中,线程1通过调用 setFlag 方法将 flag 设置为 true,而线程2通过调用 printFlag 方法等待 flag 变为 true。由于 flag 使用了 volatile 关键字,线程2对 flag 的修改能够被线程1及时感知,从而实现了可见性。

需要注意的是,volatile 能够解决可见性的问题,但对于复合操作(例如递增操作)仍然需要额外的同步措施,因为 volatile 不能保证原子性。如果需要保证原子性,可以考虑使用 synchronized 关键字或 java.util.concurrent.atomic 包中的原子类。

4. Java中的锁有哪些种类?请解释它们的区别。

内置锁(synchronized关键字)。

显示锁(ReentrantLock)。

乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)。

读写锁(ReadWriteLock)。

在Java中,锁的种类主要可以分为以下几类:内置锁(synchronized 关键字)、显式锁(ReentrantLock)、乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)、读写锁(ReadWriteLock)。

1. 内置锁(synchronized 关键字):

特点:

  • 使用 synchronized 关键字修饰的方法或代码块,实现了互斥锁和可见性。
  • 当一个线程获取到锁时,其他线程需要等待释放锁才能访问被保护的代码块或方法。
  • 适用于简单的同步场景,使用方便,但灵活性较差。

示例:

public synchronized void synchronizedMethod() {
   
    // 同步的代码块
}

2. 显式锁(ReentrantLock):

特点:

  • ReentrantLock 是显式锁的一种实现,提供了更灵活的锁定机制。
  • 可以实现公平锁和非公平锁,支持可中断性、超时等特性。
  • 提供了与 synchronized 不同的条件变量,可以更精细地控制线程的等待和唤醒。

示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void lockedMethod() {
   
        lock.lock();
        try {
   
            // 锁定的代码块
        } finally {
   
            lock.unlock();
        }
    }
}

3. 乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking):

  • 乐观锁:

    • 假设多个线程之间不会发生冲突,先进行操作,最后通过版本号等机制检查是否有冲突。
    • 适用于读多写少的场景,提高了并发性能,但需要解决冲突的问题。
  • 悲观锁:

    • 假设多个线程之间会发生冲突,因此在操作前先进行加锁,阻塞其他线程的访问,确保操作的原子性。
    • 适用于写多读少、对数据一致性要求较高的场景,但可能带来性能瓶颈。

4. 读写锁(ReadWriteLock):

特点:

  • ReadWriteLock 接口提供了读写分离的锁,包含一个读锁和一个写锁。
  • 多个线程同时读取数据,读锁不互斥,提高了并发性能。
  • 写锁是互斥的,确保写操作的原子性和一致性。

示例:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
   
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void readMethod() {
   
        readWriteLock.readLock().lock();
        try {
   
            // 读取的代码块
        } finally {
   
            readWriteLock.readLock().unlock();
        }
    }

    public void writeMethod() {
   
        readWriteLock.writeLock().lock();
        try {
   
            // 写入的代码块
        } finally {
   
            readWriteLock.writeLock().unlock();
        }
    }
}

区别总结:

  • 内置锁和显式锁是互斥锁,都是悲观锁。
  • 乐观锁和悲观锁的区别在于对冲突的处理方式,乐观锁先执行操作,再检查冲突,悲观锁先加锁,再执行操作。
  • 读写锁是一种特殊的悲观锁,提供了读写分离的机制,适用于读多写少的场景。

5. 什么是互斥锁和共享锁?

互斥锁和共享锁的定义。

互斥锁(Exclusive Lock / Mutex):

  • 定义: 互斥锁是一种锁机制,用于控制对共享资源的访问,确保在任意时刻只有一个线程可以持有该锁,其他线程必须等待释放锁后才能访问共享资源。互斥锁的目的是防止多个线程同时执行临界区(共享资源的访问区),从而避免数据竞争和不一致性。

  • 特点:

    • 只允许一个线程持有互斥锁。
    • 其他线程在试图获取锁时可能会被阻塞,直到锁被释放。
  • 应用场景:

    • 适用于对共享资源的写操作,或者需要独占访问的临界区。

共享锁(Shared Lock):

  • 定义: 共享锁是一种锁机制,允许多个线程同时持有该锁,以共享资源的访问。与互斥锁不同,共享锁不会阻止其他线程持有相同锁,因此多个线程可以同时读取共享资源,提高并发性。

  • 特点:

    • 允许多个线程同时持有共享锁。
    • 适用于对共享资源的读操作,多个线程可以同时读取数据。
  • 应用场景:

    • 适用于读多写少的场景,提高并发性能。

总结:

  • 互斥锁用于独占访问共享资源,一次只允许一个线程持有锁。
  • 共享锁允许多个线程同时持有锁,适用于读多写少的场景,提高并发性。

举例说明在多线程环境下如何使用互斥锁和共享锁。

1. 互斥锁的示例:

假设有一个共享资源 counter,多个线程同时对其进行增加操作,为了避免并发访问导致数据不一致,可以使用互斥锁来保护临界区。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexLockExample {
   
    private int counter = 0;
    private final Lock mutexLock = new ReentrantLock();

    public void increment() {
   
        mutexLock.lock();
        try {
   
            // 临界区:对共享资源进行操作
            counter++;
        } finally {
   
            mutexLock.unlock();
        }
    }

    public int getCounter() {
   
        return counter;
    }

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

        // 创建多个线程并发调用 increment 方法
        Runnable task = () -> {
   
            for (int i = 0; i < 1000; i++) {
   
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start()
  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值