java多线程理论基础

前言:由于多线程是java基础系列的知识,没法系统的从零开始写,故决定采用面试题的形式,带着问题去学习理解。

一、理论基础

1,什么是多线程

多线程是指在一个程序中同时执行多个线程(Thread)。线程是执行程序的最小单元,它可以独立运行,并且可以与其他线程并发执行。多线程的主要目的是实现并发执行,提高程序的效率和资源利用率。

在多线程编程中,可以将程序划分为多个线程,每个线程独立执行特定的任务。多个线程可以同时执行不同的任务,从而实现并发处理。每个线程都有自己的执行路径和执行状态,可以独立访问程序的共享资源。多线程编程可以提高程序的响应性能、并发处理能力和资源利用率。

2,为什么需要多线程

CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题

  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题

  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

  1. 提高程序的执行效率:多线程可以将任务划分为多个线程并发执行,从而提高程序的处理速度和吞吐量。通过充分利用多核处理器的并行计算能力,可以同时执行多个任务,加快程序的执行速度。

  2. 改善用户体验:在用户界面的开发中,使用多线程可以实现实时响应和流畅的用户交互。例如,将耗时的任务放在后台线程中执行,保持用户界面的响应性,使用户能够继续操作其他功能。

  3. 充分利用系统资源:多线程可以有效地利用计算资源和系统资源。例如,在服务器端应用中,可以通过多线程处理并发请求,提高系统的并发处理能力和资源利用率。

  4. 异步编程:多线程可以实现异步编程模型,将耗时的操作放在后台线程中执行,使主线程能够继续执行其他任务,提高程序的并发性和响应性。

  5. 并行计算:多线程可以用于并行计算任务,将大型计算任务拆分成多个子任务,并行执行,加快计算速度。这对于科学计算、数据处理、图像处理等计算密集型应用非常有益。

  6. 实现协作和同步:多线程可以用于实现线程间的协作和同步。不同的线程可以协同工作,共同完成复杂的任务。通过合理的线程同步机制,可以确保线程之间的数据一致性和安全性。

3,并行与并发的关系

  1. 串行(Serial):任务按照顺序依次执行,一个任务执行完毕后才能执行下一个任务。任务之间是串行的关系,不存在并发和并行的情况。串行执行的优点是简单可控,但执行效率较低,无法充分利用多核处理器或并行计算资源。

  2. 并发(Concurrency):多个任务在同一时间段内交替执行,共享相同的资源。任务之间通过时间片轮转、调度算法等方式进行切换,表现出来的效果是看似同时执行,但实际上是交替执行。并发适用于资源有限或需要同时处理多个任务的场景,可以提高系统的资源利用率和响应性。

  3. 并行(Parallelism):多个任务在不同的处理器核心或计算资源上同时执行。每个任务都有独立的执行环境,彼此之间不会相互干扰。并行可以在多核处理器或分布式系统中实现,通过同时执行多个任务来加速任务的执行速度。并行适用于需要高性能和快速响应的场景,可以充分发挥硬件的并行计算能力。

总结起来,串行是任务按顺序执行,没有并发和并行;并发是多个任务在同一时间段内交替执行,共享资源;并行是多个任务在不同的计算资源上同时执行,彼此独立。

我们所说的多线程一般指并发,所以才会有一些列的线程安全等问题。

二、线程

1,如何创建多线程

1.继承Thread类:创建一个类并继承Thread类,重写run()方法来定义线程的执行逻辑,然后通过创建该类的实例来创建线程对象,并调用start()方法启动线程。

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
    }
}

// 创建线程对象并启动
MyThread thread = new MyThread();
thread.start();

2.实现Runnable接口:创建一个实现了Runnable接口的类,实现其run()方法,然后通过创建该类的实例作为参数传递给Thread类的构造方法来创建线程对象,并调用start()方法启动线程。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑
    }
}

// 创建线程对象并启动
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

用lambda表达式简化

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

public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 使用Lambda表达式定义线程的执行逻辑
        Runnable task = () -> {
            System.out.println("Hello, world!");
        };

        // 提交任务给线程池执行
        executor.submit(task);

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

3.使用Callable和Future:创建一个实现了Callable接口的类,实现其call()方法,然后通过创建该类的实例作为参数传递给ExecutorService的submit()方法来提交任务,返回一个Future对象,通过调用Future对象的get()方法来获取任务的执行结果。

注意,使用线程池创建多线程实现Callable与Runnable均可。

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程执行逻辑
        return 1;
    }
}

// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);


// 提交任务并获取Future对象
MyCallable callable = new MyCallable();
Future<Integer> future = executorService.submit(callable);

// 获取任务的执行结果
Integer result = future.get();

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

以上多种方式底层都是基于Runnable来实现的。

2,springboot中使用多线程

spring boot中除了普通的Java多线程编程方式,还提供了对异步方法的支持,可以使用@Async注解将方法标记为异步执行。需要在Spring Boot的配置类上添加@EnableAsync注解,以启用异步方法的支持。然后,在希望异步执行的方法上添加@Async注解即可。

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class MyComponent {

    @Async
    public void asyncMethod() {
        // 异步执行的逻辑
    }

}

3,ThreadLocal使用场景

ThreadLocal是Java中的一个线程本地变量,它为每个线程提供了独立的变量副本,使得每个线程都可以独立地操作自己的变量副本,互不干扰。

ThreadLocal的主要作用是解决多线程环境下的数据共享和线程安全问题。在多线程程序中,如果多个线程共享同一个变量,可能会导致数据不一致或冲突的问题。而使用ThreadLocal可以让每个线程拥有自己的变量副本,从而避免了线程之间的数据冲突。

ThreadLocal的使用非常简单。我们可以通过ThreadLocal类的静态方法ThreadLocal.withInitial()或直接实例化一个ThreadLocal对象来创建一个线程本地变量。然后,通过set()方法可以向当前线程设置一个值,通过get()方法可以获取当前线程对应的值。每个线程对ThreadLocal对象的操作都只会影响到自己线程的变量副本,互不干扰。

ThreadLocal常用于以下场景:

  1. 线程上下文的传递:在多个方法之间传递共享的上下文信息,而不需要显示传递参数。

  2. 线程安全的数据存储:为每个线程存储线程安全的数据,避免使用全局变量造成线程安全问题。

  3. 事务管理:在分布式事务中,将事务上下文与线程绑定,实现线程级别的事务隔离。

需要注意的是,使用ThreadLocal时要注意避免内存泄漏问题。由于ThreadLocal使用了线程的ThreadLocalMap来存储变量副本,如果没有及时清理ThreadLocal对象,可能会导致ThreadLocalMap中的Entry无法释放,从而造成内存泄漏。因此,使用完ThreadLocal后应该及时调用remove()方法清理ThreadLocal对象,或使用initialValue()方法提供一个初始值。

总而言之,ThreadLocal是一种解决多线程环境下数据共享和线程安全问题的有效工具,可以在多线程编程中提供更好的封装和隔离性。

4,线程的生命周期

  1. 新建(New):线程对象被创建,但还没有开始执行。

  2. 就绪状态(Runnable):线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。

  3. 运行(Running):线程进入运行状态,可以开始执行线程中的任务。

  4. 阻塞(Blocked):线程被阻塞,等待某个条件的满足,例如等待输入输出、获取锁等。

  5. 终止(Terminated):线程执行完毕或出现异常而终止。

三、锁

常见的锁分类

1,java中有哪些常见的锁

  1. synchronized锁:synchronized关键字是Java中最基本的锁机制。它可以用来修饰方法或代码块,实现对方法或代码块的互斥访问。synchronized锁是隐式锁,当线程进入synchronized代码块时,会自动获取锁,执行完毕后释放锁。

  2. ReentrantLock锁:ReentrantLock是Java中的显式锁,通过lock()和unlock()方法来获取和释放锁。相比于synchronized锁,ReentrantLock提供了更多的高级功能,如可重入性、公平性、条件变量等。

  3. ReadWriteLock锁:ReadWriteLock是一种读写锁,允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。它包含一个读锁和一个写锁,通过读锁和写锁的获取和释放来实现对共享数据的并发访问控制。

  4. StampedLock锁:StampedLock是Java 8引入的一种乐观读写锁,它可以提供更高的并发性能。StampedLock支持读锁、写锁和乐观读操作,读锁和写锁之间是互斥的,但读锁和乐观读操作之间是不互斥的。

  5. Atomic类:Java.util.concurrent.atomic包中提供了一些原子类,如AtomicInteger、AtomicLong等。这些类利用底层的CAS(Compare-and-Swap)操作实现了无锁的线程安全操作,可以用于实现简单的线程同步和并发控制。

常用的synchronized与ReentrantLock的区别

2,公平锁与非公平锁

公平锁(Fair Lock)是指多个线程按照它们发出请求的顺序来获取锁。当多个线程等待同一个锁时,公平锁会按照线程的申请顺序依次获得锁。换句话说,公平锁能够保证线程获取锁的顺序与它们的请求顺序一致。公平锁遵循先来先服务的原则,确保每个线程都有公平的机会获取锁。公平锁能够防止饥饿现象的发生,但由于需要维护一个有序的线程队列,可能会带来一定的性能开销。

非公平锁(Nonfair Lock)则没有先来先服务的限制,线程在尝试获取锁时,不考虑其他线程的等待情况,直接尝试获取锁。如果当前锁没有被其他线程持有,那么线程可以立即获得锁。如果锁已经被其他线程持有,那么线程将进入竞争状态,有可能抢占到锁。非公平锁相比于公平锁,在性能上有一定的优势,因为它允许线程通过抢占的方式更快地获取锁,不需要等待其他线程释放锁。

在Java中,ReentrantLock 是一个可重入的互斥锁,它可以被设置为公平锁或非公平锁,默认情况下是非公平锁。可以通过 ReentrantLock 的构造方法来指定锁的公平性。

3,synchronized 锁升级

synchronized 是一种内置的锁机制,用于实现线程的同步和互斥。在不同的情况下,synchronized 锁可以进行锁的升级,以提供更好的性能和灵活性。

synchronized 锁的升级过程可以分为以下几个阶段:

1. 无锁状态(无锁)

当线程访问一个无同步块的代码时,处于无锁状态。多个线程可以同时进入该代码区域,没有互斥的限制。

2. 偏向锁状态(Biased Locking)

当只有一个线程访问同步块时,JVM 会将锁定的对象标记为偏向锁,并将线程 ID 记录在对象头中。此时,后续进入同步块的线程会检查对象头,如果是自己的线程 ID,表示可以直接获取锁,无需进行互斥操作。这种情况下,线程的进入和退出同步块都不会涉及到互斥。

3. 轻量级锁状态(Lightweight Locking)

当有多个线程访问同步块时,偏向锁会升级为轻量级锁。轻量级锁使用 CAS(Compare and Swap)操作来实现线程之间的互斥,避免了传统的互斥操作(如互斥量、信号量等)带来的性能开销。当只有一个线程持有轻量级锁时,其他线程可以自旋等待,避免线程的阻塞和唤醒。

4. 重量级锁状态(Heavyweight Locking)

如果自旋等待的线程仍然无法获取锁,轻量级锁会升级为重量级锁。重量级锁使用传统的互斥量机制,涉及到线程的阻塞和唤醒操作。

锁的升级是为了在不同场景下提供合适的性能和资源消耗。在竞争不激烈的情况下,使用偏向锁和轻量级锁可以减少互斥操作的开销,提高程序的执行效率。而在竞争激烈的情况下,使用重量级锁可以保证线程的正确同步和互斥,避免数据的错误和不一致。

需要注意的是,锁的升级是由 JVM 自动完成的,开发人员无需显式地干预。JVM 根据线程的竞争情况和同步块的访问模式自动选择适合的锁级别。

然而,虽然锁升级机制可以提高性能,但在某些情况下也可能引起性能问题。例如,在竞争激烈的场景下,频繁地进行锁的升级和降级操作可能会导致性能下降。因此,对于需要更细粒度控制的场景,可以考虑使用其他锁机制,如 ReentrantLock。ReentrantLock 是 JDK 提供的一个可重入锁,它具有更灵活的锁特性,可以手动控制锁的升级和降级,以满足特定需求。但需要注意的是,相比于 synchronized,ReentrantLock 的使用复杂度较高,需要手动释放锁资源,确保线程的正确同步和互斥。

4,synchronized 和ReentrantLock的优缺点及场景

synchronized 的优点:

  1. 简单易用:synchronized 是 Java 内置的关键字,使用方便,不需要显式地创建锁对象。

  2. 自动释放锁:synchronized 在执行完同步代码块或同步方法后会自动释放锁,避免了手动释放锁可能导致的遗忘或错误。

synchronized 的缺点:

  1. 不可中断:一旦一个线程获得了 synchronized 锁,其他想要获得该锁的线程只能等待,无法被中断。

  2. 不灵活:synchronized 的锁是非公平锁,无法手动控制锁的获取和释放顺序,只能按照隐式规则进行。

  3. 只支持非公平锁:synchronized 只支持非公平锁,无法灵活选择锁的公平性。

synchronized 的使用场景:

  1. 简单的线程同步:synchronized 是 Java 中最基本的线程同步机制,适用于简单的线程同步需求,如保护共享变量的访问。

  2. 实例方法同步:synchronized 可以修饰实例方法,实现对实例对象的同步访问,确保同一时间只有一个线程访问该实例的同步方法。

  3. 静态方法同步:synchronized 还可以修饰静态方法,实现对类级别的同步访问,确保同一时间只有一个线程访问该类的同步静态方法。

  4. 对象锁:synchronized 可以使用对象作为锁,实现对指定对象的同步访问,避免多个线程同时访问该对象的临界区。

ReentrantLock 的优点:

  1. 可中断:ReentrantLock 提供了可中断的获取锁的方式,即在等待锁的过程中可以响应中断请求。

  2. 公平性选择:ReentrantLock 支持公平锁和非公平锁的选择,可以手动控制锁的获取顺序。

  3. 条件唤醒:ReentrantLock 提供了 Condition 接口,可以实现更灵活的线程间通信,比如等待、唤醒等操作。

ReentrantLock 的缺点:

  1. 复杂性:相比于 synchronized,ReentrantLock 的使用复杂度较高,需要手动获取和释放锁资源,容易出现遗忘或错误。

  2. 可能导致死锁:由于 ReentrantLock 提供了更灵活的锁控制,需要手动释放锁资源,若处理不当可能导致死锁的发生。

ReentrantLock 的使用场景:

  1. 高级线程同步需求:ReentrantLock 提供了更多高级的线程同步功能,如可中断锁、公平性选择、条件等待等,适用于复杂的线程同步需求。

  2. 可中断需求:ReentrantLock 的 lock() 方法可以响应中断请求,可以方便地处理线程中断操作。

  3. 公平性选择:ReentrantLock 支持公平锁和非公平锁的选择,可以手动控制锁的获取顺序,适用于需要公平性的场景。

  4. 条件等待和唤醒:ReentrantLock 可以配合 Condition 接口实现更灵活的线程间通信,可以通过条件等待和唤醒机制实现更精细的线程控制。

综上所述,synchronized 简单易用, 适用于简单的线程同步需求和对象级别的锁控制,而 ReentrantLock 提供了更高级的功能,如可中断、公平性选择、条件唤醒等,更适合于复杂的线程同步需求和更灵活的线程控制。

5,如何避免死锁

死锁(Deadlock)是指在多线程编程中,两个或多个线程互相持有对方所需要的资源,并且都在等待对方释放资源,导致所有线程都无法继续执行的一种情况。

死锁通常发生在以下四个条件同时满足时:

1. 互斥条件(Mutual Exclusion):至少有一个资源被线程独占,其他线程无法同时访问。

2. 请求与保持条件(Hold and Wait):线程持有至少一个资源,并且在等待获取其他线程持有的资源。

3. 不可剥夺条件(No Preemption):线程持有的资源无法被其他线程强制性地剥夺,只能由线程自愿释放。

4. 循环等待条件(Circular Wait):存在一个等待链,每个线程都在等待下一个线程所持有的资源。

当这些条件同时满足时,就可能发生死锁。如果发生了死锁,那么所有涉及的线程将被阻塞,无法继续执行,程序可能会陷入无响应状态,需要手动干预解除死锁。

开发中可以采取以下几种方法来避免死锁的发生:

  1. 避免循环等待:尽量避免线程之间循环依赖资源的获取。可以通过约定资源获取的顺序,使得线程按照相同的顺序获取资源,从而避免循环等待的情况发生。

  2. 破坏持有并等待条件:要求线程在获取资源之前先释放已经持有的资源,然后再获取所需的资源。这样可以避免一个线程持有资源并等待其他线程释放资源的情况。

  3. 使用超时机制:在获取锁资源时设置超时时间,在等待超过一定时间后放弃获取锁,以避免长时间等待造成的死锁。可以使用 tryLock() 方法来实现这一机制。

  4. 使用锁的顺序:当多个线程需要获取多个锁时,要确保所有线程按照相同的顺序获取锁,以避免不同的顺序导致的死锁。可以按照固定的顺序获取锁资源,或者使用 tryLock() 方法获取锁并在获取失败时释放已持有的锁,然后重新尝试获取。

  5. 使用并发工具类:Java 提供了一些并发工具类,如 java.util.concurrent 包中的 Lock、Semaphore、Condition 等,它们提供了更灵活的线程同步和资源管理机制,可以更好地避免死锁的发生。

  6. 尽量减少同步的范围:在编写多线程代码时,尽量减少需要同步的代码块的范围,以减少竞争和等待资源的可能性。

  7. 定期检测和处理死锁:可以通过定期检测系统中是否存在死锁,并采取相应的措施解除死锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值