JAVA多线程面试题(随缘更新)

1. 什么是进程?什么是线程?

  进程和线程是计算机中两个重要的执行单元,用于实现并发执行和多任务处理。它们在操作系统和编程中扮演着不同的角色。

  1. 进程(Process):
      进程是操作系统中的基本执行单元,可以看作是程序的一个执行实例。每个进程都有独立的地址空间、数据、代码和系统资源,如文件句柄、网络连接等。进程之间相互独立,一个进程的崩溃不会影响其他进程的执行。进程是多任务处理的基础,操作系统通过时间片轮转或优先级等调度算法,使多个进程交替执行,给用户带来了同时运行多个程序的错觉。
      每个进程都有唯一的进程标识符(Process ID,PID)来进行标识。创建一个新进程通常是由操作系统的fork或spawn系统调用完成。
    打开windows任务管理器就可以看到PID,用来唯一标识一个进程

  2. 线程(Thread):
      线程是进程内的执行单元,是进程的一部分。一个进程可以包含多个线程,它们共享进程的地址空间和系统资源。线程是操作系统调度的基本单位,一个进程内的多个线程之间可以更轻松地共享数据和通信。由于线程共享进程的资源,所以创建和销毁线程的开销比创建和销毁进程要小。
      线程的引入使得程序的执行更加高效,例如在图形用户界面(GUI)应用程序中,可以使用主线程处理用户交互事件,而另一个线程可以同时处理耗时的计算任务,这样可以避免应用程序的界面在计算过程中出现卡顿。
      线程的调度是由操作系统内核负责的,不同的操作系统可能采用不同的线程调度策略。

总结:

  • 进程是操作系统中的基本执行单元,每个进程拥有独立的地址空间和系统资源,进程之间相互独立。
  • 线程是进程内的执行单元,一个进程可以包含多个线程,它们共享进程的资源,线程之间更容易共享数据和通信。
  • 进程之间的切换开销较大,但进程提供了更高的隔离性;线程之间的切换开销较小,但线程之间需要注意同步和资源共享的问题。
  • 多进程和多线程都是实现并发执行和多任务处理的方式,根据具体的应用场景和需求来选择使用进程还是线程。

2. 什么是并发?什么是并行?

  并发(Concurrency)和并行(Parallelism)是两个与多任务处理相关的概念,它们都涉及多个任务同时执行,但在执行方式和实现上有所不同。

  1. 并发(Concurrency):
      并发是指多个任务交替执行的过程。在并发处理中,多个任务轮流占用处理器的执行时间片,以看似同时执行的方式运行。操作系统通过时间片轮转或优先级调度算法,让多个任务交替执行,从而给用户带来同时运行多个任务的感觉。但实际上,每个任务在某个时间片内只能执行一部分,然后切换到下一个任务。

  并发处理通常用于提高程序的响应性和资源利用率,尤其在I/O密集型的应用场景下,例如网络通信、图形界面等。

  1. 并行(Parallelism):
      并行是指多个任务同时执行的过程。在并行处理中,多个任务可以在不同的处理器核心或计算资源上同时进行。每个任务都有自己的执行路径,可以独立执行,不会相互干扰。

  并行处理通常用于提高程序的运算速度,尤其在CPU密集型的应用场景下,例如大规模计算、科学计算等。

总结:

  • 并发是多个任务交替执行,任务之间共享处理器的执行时间片,以看似同时运行的方式进行。
  • 并行是多个任务同时执行,每个任务拥有自己的执行路径和资源,可以在不同的处理器核心或计算资源上同时运行。
  • 并发通常用于提高程序的响应性和资源利用率,而并行通常用于提高程序的运算速度。
  • 并发和并行可以结合使用,通过多线程和多进程的组合来充分利用多核处理器,既实现并行计算又实现并发任务执行。

3. 守护线程和普通线程区别

  在Java中,有两种类型的线程:守护线程(Daemon Thread)和普通线程(Normal Thread)。它们在运行行为和特点上有以下区别:

  1. 生命周期:

    • 普通线程:普通线程的生命周期不受其他线程的影响,只有在其任务执行完成或抛出未捕获异常时才会终止。
    • 守护线程:守护线程的生命周期依赖于是否还有普通线程在运行。如果所有的普通线程都已经结束,守护线程会自动终止,即使守护线程的任务未执行完毕。
  2. 线程优先级:

    • 普通线程:普通线程默认的优先级是5(Thread.NORM_PRIORITY),范围为1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY)。
    • 守护线程:守护线程的优先级与其父线程一样,通常与普通线程的优先级相同。
  3. 线程作用:

    • 普通线程:普通线程用于执行应用程序的核心任务,当有普通线程在运行时,JVM不会退出。
    • 守护线程:守护线程通常用于执行后台任务,例如垃圾回收(Garbage Collection)等。当所有的普通线程结束后,守护线程会自动终止,主要用于为其他线程提供服务和支持。
  4. 设置方法:

    • 普通线程:通过new Thread()创建的线程默认是普通线程。
    • 守护线程:通过setDaemon(true)方法将线程设置为守护线程。在启动线程之前调用该方法,将线程设置为守护线程。

  下面是设置守护线程的示例代码:

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread normalThread = new Thread(() -> {
            // 执行普通线程任务
            for (int i = 1; i <= 5; i++) {
                System.out.println("普通线程执行:" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("普通线程执行完毕!");
        });

        Thread daemonThread = new Thread(() -> {
            // 执行守护线程任务
            while (true) {
                System.out.println("守护线程执行...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        daemonThread.setDaemon(true); // 设置守护线程

        normalThread.start();
        daemonThread.start();
    }
}

  在上述例子中,普通线程执行5次后结束,而守护线程将一直运行直到JVM退出,因为没有其他普通线程在运行。如果将守护线程设置为普通线程(daemonThread.setDaemon(false);),那么它会一直执行,直到手动停止程序。

总结:

  • 普通线程的生命周期与应用程序的其他线程相互独立,它执行完任务后不会影响程序的终止
  • 守护线程的生命周期依赖于是否还有普通线程在运行,当所有普通线程结束时,守护线程会自动终止。它主要用于执行后台任务,为其他线程提供支持。

4. Java 中实现线程方式

  在Java中,有两种主要的方式来实现线程:

  1. 继承 Thread 类:
      这是最基本的实现线程的方式。你可以创建一个自定义的类,继承Thread类,并重写run()方法,在run()方法中定义线程要执行的任务。然后通过创建该自定义类的实例来启动线程。
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }

    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start(); // 启动线程1
        thread2.start(); // 启动线程2
    }
}
  1. 实现 Runnable 接口:
      这种方式是更推荐的实现线程的方式,因为Java中只支持单继承,所以通过实现Runnable接口可以更灵活地组织代码结构。与继承Thread类不同,实现Runnable接口需要实现run()方法,然后将实现了Runnable接口的对象传递给Thread类的构造函数来创建线程。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

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

        thread1.start(); // 启动线程1
        thread2.start(); // 启动线程2
    }
}
  1. 使用 Callable 和 Future(带返回值的线程):
      Callable 接口是用于支持带返回值的多线程任务的一种方式,它在 java.util.concurrent 包中定义。通过实现 Callable 接口,并将其提交给线程池,可以在任务执行完成后获取返回结果。
import java.util.concurrent.*;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程要执行的任务
        int result = 0;
        for (int i = 1; i <= 10; i++) {
            result += i;
        }
        return result;
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        MyCallable myCallable = new MyCallable();

        // 提交任务给线程池并获取 Future 对象
        Future<Integer> future = executor.submit(myCallable);

        try {
            // 获取任务的执行结果
            int result = future.get();
            System.out.println("计算结果:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

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

  无论是继承 Thread 类、实现 Runnable 接口,还是使用 Callable 和 Future,都可以实现多线程编程。但推荐使用实现 Runnable 接口或使用 Callable 和 Future 的方式,因为这样更灵活,并且可以避免由于继承 Thread 类而无法继续继承其他类的限制。

5. Runnable 和 Callable 区别

  Runnable 和 Callable都是 Java 中用于支持多线程的接口,它们有以下区别:

  1. 返回值:

    • Runnable:Runnable接口代表一个可以被线程执行的任务,但是它没有返回值,其 run()方法的返回类型是void,所以任务执行后不能返回结果。
    • Callable:Callable接口也代表一个可以被线程执行的任务,但是它有返回值,其 call()方法的返回类型是一个泛型,可以指定任务执行后的返回值类型。
  2. 抛出异常:

    • Runnable:run()方法不能抛出受检查异常,只能使用try-catch捕获异常或者在方法内部处理异常。
    • Callable:call() 方法可以抛出受检查异常,调用者在获取任务执行结果时,必须显式处理可能抛出的异常。
  3. 使用方式:

    • Runnable:通常通过实现Runnable接口,然后将实现了 Runnable接口的对象传递给 Thread类的构造函数,创建线程并启动。
    • Callable:通常结合线程池使用,通过将 Callable任务提交给线程池来执行,并通过 Future 接口来获取任务的执行结果。
  4. JDK版本:

    • Runnable:Runnable 接口是从 JDK 1.0 就开始存在的。
    • Callable:Callable 接口是从 JDK 1.5 的并发包 java.util.concurrent 中引入的。

总结:

  • Runnable 和 Callable 都是用于支持多线程的接口,区别在于 Runnable 不返回结果且不能抛出受检查异常,而 Callable 可以返回结果且可以抛出受检查异常
  • Runnable 通常与 Thread 类一起使用,而 Callable 通常与线程池和 Future结果对象一起使用。
  • Runnable 从 JDK 1.0 开始存在,而 Callable 是从 JDK 1.5 的并发包中引入的。

6. run 和 start 区别

  在 Java 中,run() 和 start() 是用于启动线程的两种方法,它们有以下区别:

  1. run() 方法:
    • run()方法是线程的实际执行代码,包含了线程要执行的任务逻辑。
    • 当直接调用run() 方法时,它会在当前线程中按照顺序执行,不会创建新的线程,只是普通的方法调用而已。
    • 因此,直接调用 run() 方法不会启动新的线程,任务会在当前线程中顺序执行完毕后才返回。
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.run(); // 直接调用 run() 方法,任务在当前线程中顺序执行
    }
}
  1. start() 方法:
    • start() 方法用于启动新的线程,并让新线程执行 run() 方法中的任务逻辑。
    • 当调用 start() 方法时,JVM 会创建一个新的线程,并在新线程中调用 run() 方法,使得任务在新线程中并发执行。
    • 因此,start() 方法是用于启动新线程的,不会阻塞当前线程。
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running: " + i);
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动新线程,任务在新线程中并发执行
    }
}

总结:

  • run() 方法是线程的实际执行代码,直接调用它时在当前线程中顺序执行任务。
  • start() 方法用于启动新线程,并在新线程中并发执行 run() 方法中的任务逻辑。
  • 为了启动新线程并执行任务,应该调用 start() 方法,而不是直接调用 run() 方法。
  • 概括为一句话就是:线程是通过 Thread 对象所对应的方法 run() 来完成其操作的,而线程的启动是通过 start() 方法执行的。 run() 方法可以重复调用,start() 方法只能调用一次。

7. sleep() 方法和 wait() 方法对比

sleep() 方法和 wait() 方法是用于线程控制的两种不同方式,它们在使用场景和功能上有一些区别:

  1. sleep() 方法:

    • sleep() 方法是 Thread 类的一个静态方法,它可以让当前线程休眠一段指定的时间(以毫秒为单位)。
    • 在调用 sleep() 方法期间,线程会暂时释放CPU资源,但它仍然持有对象的锁(如果有的话)。
    • 当休眠时间结束后,线程会进入就绪状态,等待获取CPU资源再次执行。
  2. wait() 方法:

    • wait() 方法是 Object 类的一个方法,也可以通过 Thread 类访问。它用于在多线程环境中进行线程间的通信。
    • 当一个线程调用某个对象的 wait() 方法时,它会释放对象的锁,并进入该对象的等待队列,等待其他线程调用相同对象的 notify()notifyAll() 方法来唤醒它。
    • wait() 方法通常与 synchronized 关键字一起使用,以确保线程在等待时不会发生并发问题。

主要区别:

  • sleep() 是线程的一个静态方法,而 wait()Object 类的方法。
  • sleep() 让线程休眠一段指定时间后自动恢复执行,而 wait() 是让线程进入等待状态,需要其他线程显式地唤醒它。
  • 在调用 sleep() 方法期间,线程并不释放它所持有的对象的锁,而调用 wait() 方法后,线程会释放对象的锁,让其他线程可以获取锁并执行。

使用场景:

  • sleep() 适用于需要让线程暂停执行一段时间的情况,如实现简单的定时功能。
  • wait() 适用于多线程间需要通信、协调工作的情况,通过 wait()notify() 实现线程间的同步。

总结:

  • sleep() 方法没有释放锁,而wait()方法释放了锁释放了锁
  • wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep()是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

8. synchronized 关键字的使用方式

  synchronized 关键字在 Java 中有三种常见的使用方式:

  1. 同步实例方法(Synchronized Instance Method):
      在方法声明中使用synchronized 关键字来修饰实例方法,这样整个方法就被视为一个同步代码块,保证同一时刻只能有一个线程访问该方法。其他线程在调用该方法时,如果发现该方法被锁定,它们就会等待锁的释放。在这种方式下,锁对象是当前对象实例
public class MyClass {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
  1. 同步静态方法(Synchronized Static Method):
       在静态方法的声明中使用 synchronized 关键字来修饰,这样整个静态方法就被视为一个同步代码块,保证同一时刻只能有一个线程访问该方法。其他线程在调用该静态方法时,如果发现该方法被锁定,它们就会等待锁的释放。在这种方式下,锁对象是当前类的 Class 对象
public class MyClass {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}
  1. 同步代码块(Synchronized Block):
      在代码块中使用 synchronized 关键字来修饰一段代码,这样只有拥有相同锁对象的线程才能同时执行这段代码。通常,锁对象是一个共享资源,通过使用同一个锁对象来实现线程间的同步。
public class MyClass {
    private Object lock = new Object();
    private int count = 0;

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

  在以上三种方式中,synchronized关键字保证了多线程对共享资源的互斥访问,确保同一时刻只有一个线程能够执行被同步的代码块。注意在使用 synchronized 时,要确保锁对象是共享的,并且所有线程都能获得同一个锁对象,否则同步可能不会起作用。另外,要避免在不必要的情况下过度使用 synchronized,以减少性能开销。

9. 什么是 ThreadLocal

ThreadLocal 是 Java 中的一个线程局部变量,它允许在多线程环境下每个线程都拥有自己独立的变量副本。换句话说,ThreadLocal提供了一种在每个线程中存储数据的机制,确保数据对其他线程是不可见的,避免了线程间数据共享导致的并发问题。

常见的用途是在多线程环境下,为每个线程提供独立的实例,这样每个线程可以独立地操作自己的数据而不会相互影响。通常情况下,ThreadLocal 变量使用 private static 修饰,保证其线程隔离性。

ThreadLocal 提供了以下主要方法:

  1. set(T value):将当前线程的局部变量设置为指定的值 value。
  2. get():获取当前线程的局部变量值。
  3. remove():将当前线程的局部变量删除,清除当前线程中的值。

以下是一个简单的示例,展示了如何在每个线程中存储独立的值:

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

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int value = (int) (Math.random() * 100);
            threadLocal.set(value);
            System.out.println(Thread.currentThread().getName() + " set value: " + value);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " get value: " + threadLocal.get());
        };

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

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

  在上述示例中,我们创建了一个 ThreadLocal 变量 threadLocal,并在两个线程中使用它来存储独立的值。运行程序后,你会发现每个线程都拥有自己的变量副本,而不会相互干扰。

  在实际案例中的使用(个人案例使用:共享用户登录)
保存用户信息:

  //类中定义一个ThreadLocal
   public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        HttpSession session = request.getSession();
        MemberRespVo attribute = (MemberRespVo)session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            //向ThreadLocal保存用户登录信息
            loginUser.set(attribute);
            return true;
        }else{
            //没登录
            request.getSession().setAttribute("msg","请先登录");
            response.sendRedirect("http://home.com/login.html");
            return false;
        }
    }

调用get方法获取信息

   @Override
    public PageUtils queryPageWithItem(Map<String, Object> params) {
    	//获取用户信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>().eq("member_id",memberRespVo.getId()).orderByDesc("id")
        );
        //查询订单详情项
        List<OrderEntity> collect = page.getRecords().stream().map(item -> {
            List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", item.getOrderSn()));
            item.setItemEntities(itemEntities);
            return item;
        }).collect(Collectors.toList());

        page.setRecords(collect);

        return new PageUtils(page);
    }

总结:

  • ThreadLocal 是 Java 中的一个线程局部变量,为每个线程提供了独立的变量副本。
  • 通过 set() 方法设置当前线程的局部变量值,通过 get() 方法获取当前线程的局部变量值
  • ThreadLocal主要用于在多线程环境下为每个线程提供独立的实例,避免线程间数据共享带来的并发问题。

10. Java线程的生命周期

  1. 新建(New): 线程对象被创建,但尚未启动。此时线程处于新建状态。

  2. 可运行(Runnable): 调用线程对象的start()方法后,线程进入可运行状态。在可运行状态下,线程已准备好执行,但可能还未获得CPU资源。

  3. 运行(Running): 在可运行状态下,线程可能会获得CPU资源并真正开始执行,进入运行状态。此时线程的run()方法中的代码正在被执行。

  4. 阻塞(Blocked): 在运行状态下,线程可能会由于某些原因被阻塞,导致线程暂时停止执行,进入阻塞状态。常见的原因包括线程等待某个资源、线程进入synchronized块时被其他线程锁定等。

  5. 等待(Waiting): 在某些情况下,线程可能会进入等待状态。线程可以通过Object.wait()Thread.join()或类似方法等待一段时间或直到其他线程通知它恢复执行。

  6. 终止(Terminated): 线程可以通过return正常终止或通过抛出未捕获的异常非正常终止。在任何情况下,一旦线程的run()方法执行完毕,线程都会进入终止状态。

  这些状态表示了线程从新建到终止的完整生命周期,线程的状态由Java虚拟机(JVM)来管理和控制。通过合理地管理线程的状态,可以确保多线程程序的正确运行。
在这里插入图片描述

11. 线程安全是什么,要怎么确保

  Java线程安全是指在多线程环境下,多个线程对共享资源的访问不会导致数据的不一致或出现意外结果。当多个线程同时访问共享资源时,如果不采取适当的同步措施,可能会导致数据竞争和不确定的行为。

  线程安全的实现要确保在并发访问时,共享资源的状态能够正确地保持一致,而不会发生数据污染、丢失或损坏。为了实现线程安全,通常会使用同步机制,例如(具体操作根据技术栈来决定,主要给个思路):

  1. 互斥锁(Mutex): 使用synchronized关键字或ReentrantLock类来确保同一时间只有一个线程可以访问共享资源,其他线程需要等待锁的释放。

  2. 原子操作(Atomic Operations): 使用Java的原子类(例如AtomicInteger、AtomicLong等)来执行一些基本操作,确保这些操作是原子的,不会被其他线程中断。

  3. 并发集合(Concurrent Collections): 使用Java的并发集合类(例如ConcurrentHashMap、ConcurrentLinkedQueue等)来替代非线程安全的集合类,以支持多线程并发访问。

  4. 线程本地存储(Thread-Local Storage): 使用ThreadLocal类来确保每个线程都有自己独立的变量副本,不会被其他线程共享。

  5. 不可变对象(Immutable Objects): 使用不可变对象可以避免在多线程环境下修改共享状态的问题,从而实现线程安全。

总结:

  • Java线程安全是指在多线程环境下,多个线程对共享资源的访问不会导致数据的不一致或出现意外结果,不安全就是出现了以外结果,简单来说就是如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  • 保证线程安全的操作(具体操作还是需要看系统设计,上述方法只是讲解了Java中可以实现的方式,不是唯一,就比如可以使用redission来实现互斥锁,更加常用一些):
    1.互斥锁
    2.原子操作
    3.并发合集
    4.线程本地存储
    5.不可变对象

12. 什么是死锁

死锁(Deadlock)是计算机科学中一个重要的概念,通常用于描述多个进程或线程在竞争资源时可能出现的一种僵局状态。

在并发编程中,多个进程或线程可能会竞争访问共享资源,比如内存、文件、打印机等。死锁指的是这样一种情况:每个进程或线程都在等待其他进程或线程持有的资源,同时又不愿意释放自己当前持有的资源,导致所有参与者都无法继续前进,陷入了无限等待的状态。

死锁的产生必须满足以下几个条件,称为死锁的必要条件:

  1. 互斥条件(Mutual Exclusion):资源一次只能被一个进程或线程占用,如果一个资源已经被占用,其他进程或线程就无法访问该资源。

  2. 请求与保持条件(Hold and Wait):进程或线程在持有至少一个资源的同时,又请求额外的资源。

  3. 不可剥夺条件(No Preemption):资源不能被抢占,只能在持有资源的进程或线程主动释放后才能被其他进程或线程获取。

  4. 循环等待条件(Circular Wait):多个进程或线程形成一个资源请求的循环链,每个进程或线程都在等待下一个进程或线程所持有的资源。

一旦出现了上述四个条件,就可能发生死锁。死锁是一个严重的问题,因为它会导致系统无响应,无法继续执行任务,只能通过重启或其他手段解决。在设计并发系统时,需要采取合适的策略来避免死锁的发生,比如资源有序分配、避免循环等待等方法。

总结:

一句话:死锁问题:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。

13. 线程池:

13.1 什么是线程池

  线程池(Thread Pool)是一种并发编程的技术,用于管理和复用线程,以提高多线程应用程序的性能和效率。在传统的多线程编程中,每次需要执行一个任务时都会创建一个新的线程,任务执行完毕后线程会被销毁,这样频繁地创建和销毁线程会消耗大量系统资源。
  线程池通过预先创建一组线程,并将任务提交给这些线程来执行,从而避免了频繁创建和销毁线程的开销。线程池中的线程会一直存在,它们会反复地执行新的任务,直到线程池被显式地关闭。
  线程池,本质上是一种对象池,用于管理线程资源。 在任务执行前,需要从线程池中拿出线程来执行。在任务执行完成之后,需要把线程放回线程池。 通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。

13.2 实现线程池的好处

  1. 降低线程创建和销毁的开销:线程池中的线程可以被复用,避免了频繁地创建和销毁线程,提高了执行任务的效率。
  2. 控制并发线程数量:线程池可以限制并发线程的数量,防止线程过多导致系统资源过度占用。
  3. 提高响应性:线程池可以快速响应新的任务,减少任务执行的延迟。

13.3 什么是核心线程池、线程池、队列(任务队列)

  1. 核心线程池(Core Thread Pool):
    核心线程池是线程池中的最小线程数量它在线程池的整个生命周期内始终保持活跃。无论是否有任务需要执行,核心线程池中的线程都会存在,并且不会被销毁,以减少线程的创建和销毁开销。核心线程池通常是线程池中的起始线程数量,线程池的大小可以动态调整,但不会低于核心线程池的大小。

  2. 线程池:
    线程池是一个管理线程的池子,用于管理和复用线程,以提高多线程应用程序的性能和效率。它由一组线程和任务队列组成。线程池会在初始化时创建一定数量的线程,并根据任务的提交情况动态地调整线程数量。线程池会从任务队列中获取任务并将其分配给空闲的线程来执行。线程池还支持拒绝策略,用于处理线程池饱和时无法处理的新任务。(可以理解为最大接受线程量)

  3. 队列(任务队列):
    队列是线程池中用于存储待执行任务的数据结构。当任务提交给线程池时,它会被放入任务队列中,等待线程池中的工作线程来获取并执行。任务队列充当了线程池和任务之间的缓冲区,可以减轻线程的竞争和调度开销。不同的线程池实现可能使用不同类型的队列,比如链表队列、数组队列、优先级队列等,来满足不同的需求。

13.4 线程池的基本流程

  1. 判断核心线程池是否已满,如果不是,则创建线程执行任务
  2. 如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中
  3. 如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务
  4. 如果线程池也满了,则按照拒绝策略对任务进行处理。

13.5 线程池的拒绝策略

  1. AbortPolicy(默认策略):这是默认的拒绝策略。当线程池无法接受新的任务时,会抛出 RejectedExecutionException 异常,阻止新任务的提交。
  2. CallerRunsPolicy:当线程池无法接受新的任务时,新提交的任务会由提交任务的线程来执行该任务。这样做可以保证任务不会丢失,但会影响提交任务的性能。
  3. DiscardPolicy:当线程池无法接受新的任务时,新提交的任务会直接被丢弃,不会抛出异常,也不会执行。
  4. DiscardOldestPolicy:当线程池无法接受新的任务时,会丢弃任务队列中最早提交的任务,然后尝试重新提交新任务。

14. 什么是CAS

CAS是"Compare and Swap"(比较并交换)的缩写,是一种用于实现多线程同步的原子操作。在并发编程中,多个线程同时访问共享资源可能会导致数据不一致和竞态条件等问题。CAS是一种乐观锁定机制,用于解决这些问题,并确保多线程间对共享资源的操作是安全的。

CAS操作涉及三个主要步骤:比较、读取和更新。它的基本思想如下:

  1. 比较:首先,CAS会比较当前共享资源的值与期望的值是否相等。

  2. 读取:如果比较结果为相等,那么表示当前共享资源的值没有被其他线程修改,CAS会读取当前的共享资源值。

  3. 更新:在读取阶段之后,CAS会尝试更新共享资源的值为新的期望值。更新只有在共享资源的值在读取阶段没有被其他线程修改的情况下才会成功,否则更新操作将失败。

如果更新成功,CAS操作是原子的,意味着整个过程没有被其他线程中断。如果更新失败,通常会采取重新尝试或其他处理方式,直到更新成功为止。

CAS操作是无锁的,相对于传统的锁机制(如synchronized关键字),CAS不会使线程进入阻塞状态,而是通过不断地重试来解决竞争条件。这使得CAS在高并发环境下表现得更加高效,但也需要开发者仔细处理CAS操作失败的情况,以保证数据的一致性和正确性。

在Java中,java.util.concurrent包提供了AtomicInteger、AtomicLong等原子类,它们使用CAS操作来实现对整数和长整数等基本数据类型的原子操作,帮助开发者编写线程安全的代码。

CAS的缺点主要有3点:

  1. ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
  2. 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
  3. 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

15. 什么是AQS

  AQS是"Abstract Queue Scheduling"(抽象队列调度)的缩写,是指Java中的一个并发编程框架。具体来说,AQS是AbstractQueuedSynchronizer的缩写,翻译过来应该是抽象队列同步器,是Java中用于构建同步器的基础框架。

  在Java并发编程中,通常需要处理多线程间的同步和协作,以保证线程安全和正确的执行顺序。AQS提供了一个灵活且强大的机制,允许开发者创建自定义的同步器。它通过一个FIFO(先进先出)队列来管理等待线程,使得线程能够按照特定的顺序获得共享资源的访问权。

  AQS的核心思想是,将同步状态(synchronization state)和等待线程(waiting threads)封装在一个队列中,这个队列以节点(Node)的形式组织。当一个线程请求共享资源时,如果资源已经被其他线程占用,该线程就会被加入到等待队列中,进入等待状态。一旦资源释放,AQS会将队列中的下一个线程唤醒,使其可以继续执行。

  ReentrantLock和CountDownLatch等Java并发工具类就是基于AQS构建的。通过使用AQS,开发者可以更方便地实现自定义的同步器,满足特定的线程同步需求。但是,需要注意的是,AQS是一个比较底层的抽象框架,一般情况下,使用Java提供的高级并发类会更为简单和安全。

  如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

  AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

16. 什么是线程上下文切换

线程上下文切换是指在多线程环境下,从一个正在运行的线程切换到另一个线程的过程,其中上下文指的是线程在执行过程中会有自己的运行条件和状态。在计算机系统中,操作系统负责管理线程的执行和调度,而线程是执行计算任务的最小单位。

当一个线程在执行过程中,可能由于以下几种情况需要进行上下文切换:

  1. 时间片用完:操作系统使用时间片轮转法等调度算法来分配给每个线程一个小的时间片,当一个线程的时间片用完后,操作系统会暂停当前线程的执行,并切换到下一个就绪状态的线程继续执行。

  2. 阻塞和唤醒:线程在执行过程中可能因为等待某些资源(如等待用户输入、等待磁盘读写等)而被阻塞,当资源准备好后,操作系统会将其唤醒并进行上下文切换。

  3. 优先级调度:当高优先级的线程抢占了CPU资源,需要将低优先级的线程切换出去。

上下文切换过程包含以下关键步骤:

  1. 保存当前线程的上下文:保存当前线程的程序计数器、寄存器内容和其他必要的执行状态,以便稍后能够恢复该线程的执行。

  2. 恢复目标线程的上下文:加载目标线程保存的上下文,将CPU寄存器和执行状态还原为目标线程最后一次执行的状态。

  3. 切换内核栈:由于每个线程都有自己的内核栈用于保存执行上下文,需要在切换时同时切换内核栈。

上下文切换是一个相对昂贵的操作,因为涉及到保存和恢复大量的执行状态。过多的上下文切换可能会导致系统性能下降。因此,合理的线程调度和优化是保证多线程应用程序高效运行的重要因素。

17. 乐观锁和悲观锁

在 Java 中,乐观锁和悲观锁是两种常见的并发控制机制,用于处理多线程环境下共享资源的访问问题。

  1. 乐观锁(Optimistic Locking):
    乐观锁假设多个线程访问共享资源时,它们大部分时间不会发生冲突,因此不会立即阻塞其他线程。当一个线程要修改共享资源时,它先获取该资源的一个版本标识(通常是一个版本号或时间戳),然后在修改完成后,检查该标识是否仍然有效。如果有效,说明在这期间没有其他线程修改该资源,那么该操作可以成功提交;否则,说明有其他线程修改了资源,当前线程的操作可能会失败,需要根据实际情况进行冲突处理。

简单一句话就是:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了。在 Java 中,乐观锁通常使用 CAS(Compare and Swap)操作来实现。

  1. 悲观锁(Pessimistic Locking):
    悲观锁假设多个线程访问共享资源时,它们可能会频繁地发生冲突,因此在访问共享资源之前,会将该资源锁定,其他线程需要等待锁释放后才能访问资源。悲观锁在资源访问期间阻塞其他线程,以确保每次只有一个线程可以修改资源,从而避免并发冲突。

简单一句话就是:悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。在 Java 中,悲观锁可以使用 synchronized 关键字或 ReentrantLock 类等来实现。当一个线程进入 synchronized 块或获取 ReentrantLock 锁时,其他线程将会阻塞直到当前线程释放锁。

总结:

一句话区分:
乐观锁在多线程访问的时候不加锁,就只有在修改的时候加;悲观锁不管什么情况,只要访问了就加锁。

18. 同步和异步

同步(Synchronous)和异步(Asynchronous)是用来描述计算机系统中不同操作执行方式的术语。

  1. 同步:
    同步指的是操作按照预定的顺序依次执行,每个操作必须等待前一个操作完成后才能开始执行。在同步操作中,调用者在发起请求后,需要一直等待操作完成并获得结果,然后才能继续执行下一步操作。

在多线程编程中,同步操作通常使用锁(比如Java中的synchronized关键字或ReentrantLock类)来保证多个线程按照特定顺序执行,以避免竞态条件(Race Condition)和数据不一致问题。

  1. 异步:
    异步指的是操作在发起请求后,不需要立即等待结果,而是可以继续执行其他操作。操作会在后台或另一个线程中进行处理,并在完成后通知调用者或回调相应的处理函数。

在异步操作中,调用者通常不会阻塞等待结果,这样可以提高系统的并发性和响应性能。异步操作常用于处理耗时的任务,例如网络请求、文件读写、数据库查询等。在Java中,异步操作可以通过多线程、回调、Future/Promise等方式来实现。

总结:
同步和异步是两种不同的操作执行方式:

  • 同步操作按照顺序依次执行,需要等待前一个操作完成后才能继续执行下一个操作。
  • 异步操作在发起请求后不需要立即等待结果,可以继续执行其他操作,并在完成后通知或处理结果。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值