线程和锁

线程和锁

一、线程的状态

在java中,线程的状态可以通过Tread类中getState()方法进行获取。以下是Java开发中常见的线程状态:

1、New(新建): 线程创建但未被启动。

2、RUNNABLE(可运行):线程可以在任意时刻运行。处于这个状态的线程可能正在运行,也可能正在等待CPU分配时间片。

3、BLOCKED(阻塞):线程被阻止执行,因为它正在等待监视器锁定。其他线程正在占用所需的锁定,因此线程被阻塞。

4、WAITING(等待):线程进入等待状态,直到其他线程显式地唤醒它。线程可以调用Object类的wait()方法、join()方法或Lock类的条件等待方法进入此状态。

5、TIMED_WAITING(计时等待):线程进入计时等待状态,等待一段指定的时间。线程可以调用Thread.sleep()方法、Object类的wait()方法、join()方法或Lock类的计时等待方法进入此状态。

6、TERMINATED(终止):线程完成了其任务,或者因为异常或其他原因而终止运行。

以上是Java开发中线程的常见状态。线程可以根据业务逻辑和操作系统的调度来在不同状态之间转换。了解线程状态对于编写并发程序和调试多线程应用程序非常重要。

总结:线程的状态包括:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)、终止(Terminated)。

二、创建线程的方式

在Java中,创建线程有以下几种方式:

  1. 继承Thread类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个继承自Thread类的线程类
class MyThread extends Thread {
public void run() {
// 线程执行的代码
System.out.println("Thread running");
}
}

// 创建线程实例,并启动线程
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
  1. 实现Runnable接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个实现Runnable接口的线程类
class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码

}
}

// 创建线程实例,并启动线程
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
  1. 使用匿名内部类:
1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
// 线程执行的代码
System.out.println("Thread running");
}
});
thread.start();
}
}
  1. 使用Lambda表达式:
1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Thread running"));
thread.start();
}
}

这些都是Java中创建线程的常见方式,你可以根据具体需求选择合适的方式来创建线程。

三、线程池的核心线程数

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,

ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  • corePoolSize => 线程池核心线程数量
  • maximumPoolSize => 线程池最大数量(包含核心线程数量)
  • keepAliveTime => 当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
  • unit => keepAliveTime 的单位
  • workQueue => 线程池所使用的缓冲队列,被提交但尚未被执行的任务
  • threadFactory => 线程工厂,用于创建线程,一般用默认的即可
  • handler => 拒绝策略,当任务太多来不及处理,如何拒绝任务

四、如何创建线程池

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  2. CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  • 创建线程的方式一:new TreadPoolExecutor 方式
1
2
3
4
5
6
ExecutorService executorService = new ThreadPoolExecutor(3,5,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 9; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + "开始办理业务了。。。。。。");
});
}
  • 创建线程池方式二:spring的ThreadPoolTaskExecutor方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration

public class ExecturConfig {

@Bean("taskExector")

public Executor taskExector() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(3);//核心池大小

executor.setMaxPoolSize(5);//最大线程数

executor.setQueueCapacity(3);//队列长度

executor.setKeepAliveSeconds(10);//线程空闲时间

executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略

return executor;

}

}

创建线程池的注意事项:

1. 线程池的大小:线程池的大小应根据任务的类型和系统资源进行合理的配置。如果线程池的大小过小,可能会导致任务排队等待执行,影响系统的响应性能;如果线程池的大小过大,可能会占用过多的系统资源,导致系统负载过重。可以通过监控和调整线程池的大小来优化性能。

2. 任务队列的选择:线程池通常会使用一个任务队列来保存待执行的任务。任务队列的选择应根据任务的特性进行合理的选择。如果任务较多且执行时间较短,可以选择无界队列(如LinkedBlockingQueue);如果任务较少且执行时间较长,可以选择有界队列(如ArrayBlockingQueue)或者优先级队列(如PriorityBlockingQueue)。

3. 线程池的拒绝策略:当任务无法被线程池接收执行时,需要定义适当的拒绝策略。常见的拒绝策略有:抛出异常(AbortPolicy)、丢弃任务(DiscardPolicy)、丢弃最早的任务(DiscardOldestPolicy)和调用者运行任务(CallerRunsPolicy)。根据业务需求选择合适的拒绝策略。

4. 线程池的生命周期管理:线程池的生命周期包括初始化、执行任务和关闭。在初始化时,需要设置线程池的参数;在执行任务时,需要提交任务到线程池;在关闭时,需要调用线程池的shutdown()或shutdownNow()方法来关闭线程池,并等待所有任务完成。正确地管理线程池的生命周期可以避免资源泄漏和线程阻塞的问题。

5. 线程安全性:在自定义线程池时,需要考虑线程安全性。多个任务并发执行时,可能会涉及到共享资源的访问,需要使用合适的同步机制来保证线程安全。

总之,自定义线程池需要合理配置线程池的大小、选择适当的任务队列和拒绝策略,正确管理线程池的生命周期,并考虑线程安全性。这些注意事项可以帮助我们设计高效、可靠的线程池。

五、线程池的工作原理

线程池在刚创建的时候并没有线程,任务队列是通过作为参数传进来的,不过,就算队列里有任务也立马去执行任务。

当调用execute()方法添加一个任务的时候,线程池会做出如下判断:

  • 如果正在运行线程的数量小于corePoolSize,那么马上创建线程池来运行这个任务;
  • 如果正在运行的线程池数量大于或等于corePoolSize,那么将会放入任务队列;
  • 如果这个时候队列满了,而且正在运行的线程数量小于maximumPooSize,那么还是要创建非核心线程来立刻执行这个任务;
  • 如果队列满了,而且正在执行的线程数量大于或等于maximumPoolSize 那么线程池会抛出异常 RejectException

当一个线程完成任务时,它会从队列中取下一个任务来执行。

当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

六、线程池大小如何设定

线程池使用面临的核心的问题在于:线程池的参数并不好配置。线程池的数量应该综合考虑CPU核心数、并发请求数量、任务类型和任务队列容量等因素。根据具体情况进行调试和压测,逐步调整线程池大小,以找到最佳配置,以提高系统性能和资源利用率。

有一个简单并且适用面比较广的公式:

CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

如果是 CPU 密集型的,可以把核心线程数设置为核心数+1。

如果是 IO 密集型的,可以把核心线程数设置 2*CPU 核心数

总结: 生产环境中,Java线程池大小的设定与硬件资源和并发需求密切相关。通常可以考虑CPU核心数、内存容量、网络带宽等硬件资源,并结合预估的并发请求量来确定线程池大小,以充分利用资源并保持合理的并发处理能力。较多的硬件资源和高并发通常需要更大的线程池来提高并发处理效率。

七、线程池的拒绝策略

默认(AbortPollcy)丢弃任务丢弃最早的任务抛出异常调用者运行
特点默认拒绝,直接抛出RejectExecutionExeception异常直接丢弃无法处理的问题丢弃线程池中最早的任务抛出RejectedExecutionException异常由提交任务的线程执行任务
适用场景默认选项,适用于大部分情况不必关心无法处理的任务对新任务优先级不高,且无法处理时,可以丢弃一部分旧的任务需要明确的知道任务被拒绝的情况可以处理任务线程较快,任务不会对线程造成太大的影响
示例代码ThreadPoolExceutor. AborPolicyThreadPoolExcecutor. DiscardOldesPolicyThreadPoolExcecutor. AbortPolicyThreadPoolExcecutor. AbortPolicyThreadPoolExcecutor. CallerRunsPolicy

以上列出的是Java线程池中常见的拒绝策略,具体可以根据实际情况选择合适的拒绝策略,也可以自定义实现RejectedExecutionHandler接口来定义自己的拒绝策略。默认的拒绝策略是AbortPolicy,即直接抛出异常。

线程池的拒绝策略有四种:AbortPolicy(默认方式,中止并抛出RejectedExecutionException异常)、CallerRunsPolicy(使用调用线程来执行被拒绝的任务)、DiscardPolicy(默默地丢弃被拒绝的任务)以及DiscardOldestPolicy(丢弃最早被添加到队列的任务,然后尝试重新提交新任务)。。如果希望快速失败并将异常传递给调用者,则选择AbortPolicy。如果希望尽可能保证任务的执行而不堆积在队列中,则选择CallerRunsPolicy。如果对任务的丢失情况不敏感,则选择DiscardPolicy。而如果希望尽可能保留最新的任务而不是旧的任务,则选择DiscardOldestPolicy。

八、synchronized 和 lock 的区别

synchronizedlock
机制和灵活性是Java内置的关键字,是内置语言的同步机制接口,提供了更多的灵活的同步机制
范围修饰的方法或代码块,可以实现整个方法或代码时自动获取锁通过lock()和unlock()方法手动夺取锁和释放后锁
底层实现原理基于Jvm的内置监视锁机(monitor)制实现的可以有多种底层实现,如ReentrantLock,StampedLock等
获取锁的方式隐式的获取锁,在synchronized方法或代码块时自动获取锁显式获取锁,需要手动调用lock()方法获取锁,然后在合适的时候进行释放锁
性能在低竞争环境下性能较好,有底层优化机制(偏向锁等)在该竞争的环境下性能较好,提供了更多的选项

总结:synchronized的是Java提供的关键字,是内置的同步机制,能够修饰方法和代码块,底层实现原理是基于Jvm的内置监锁机制;而lock是一个接口,提供了更加灵活的同步机制,可以手动的获取锁和释放锁,底层实现可以是ReentrantLock等,性能在高竞争环境下通常较好。

九、什么情况下会产生死锁、如何解决

死锁是多线程编程中常见的问题,当多个线程相互等待对方释放资源时,就可能导致死锁的发生。通常情况下,死锁发生的四个必要条件是:互斥条件、请求与保持条件、不可剥夺条件和循环等待条件。

产生死锁的情况:

  • 互斥条件:如果多个线程按照不同的顺序释放锁,并且互相依赖对方释放的锁,就可能造成死锁;
  • 资源竞争问题:当多个线程同时竞争有限的资源,例如共享的数据库连接、文件等,在资源分配不当的情况下,可能导致死锁;
  • 不可剥夺条件:资源只有由占有它的线程进行主动释放,不能被其他线程强行的剥夺;
  • 循环等待条件:存在一个进程或线程的资源等待链,使得每个进程或线程都在等待下一个资源。

\解决死锁方法:**

\1. 避免使用嵌套锁:尽量避免在一个锁内部再次申请其他锁资源,减少死锁可能性。

  1. 统一锁申请顺序:对于需要多个锁的场景,确保所有线程以相同的顺序请求锁,避免出现循环等待的情况。

  2. 加锁超时或自动释放:在申请锁时,设置一个等待时间或使用可重入锁,并且设置超时时间,避免线程长时间等待而导致死锁。同时,在使用完锁后,及时释放资源,避免持有锁时间过长。

  3. 死锁检测和恢复:通过死锁检测算法,定期检测系统中的死锁情况,并尝试解决死锁,然后恢复运行。

  4. 资源分配策略优化:评估和优化资源的分配策略,避免资源竞争和瓶颈情况的发生。

  5. 避免长时间持有锁:在代码设计中,尽量减少需要锁的代码块,避免长时间持有锁,减少死锁的机会。可以使用并发集合或并发算法来减少对锁的需求。

总的来说,解决死锁问题需要注意锁的申请顺序、资源分配策略、超时设置等,通过优化设计和避免资源竞争,可以减少死锁的可能性。在发生死锁时,通过死锁检测和恢复等方法解决死锁问题。

总结:产生死锁的情况是在多线程程序中,每个线程都持有一些资源,并且等待其他线程释放它所需的资源。解决死锁可采取以下方法:避免死锁的发生,通过破坏死锁产生的四个必要条件之一来预防;检测死锁,使用算法检测出是否存在死锁,并采取相应的措施解除死锁;恢复死锁,即进行资源的强制抢占或进行回滚操作,将进程回退到安全状态以解除死锁。

十、ThreadLocal是一个什么样的技术

ThreadLocal的实现原理:

ThreadLocal通过在每个线程中维护一个ThreadLocalMap对象来实现线程隔离。ThreadLocalMap以ThreadLocal对象作为键,线程私有的变量副本作为值。每个线程都有自己的ThreadLocalMap,线程可以通过ThreadLocal的get()和set()方法来获取和设置自己线程的ThreadLocal变量的值。

应用场景:

  1. 多线程环境下需要独立存储和获取数据的场景,例如线程池中的任务需要使用各自独立的数据库连接、计数器等。

  2. 线程上下文传递,例如Web框架中将请求信息或用户登录信息存储在ThreadLocal中,方便各层次方法调用时获取,避免了传递参数的麻烦。

坑与解决方法:

  1. 内存泄漏问题:由于ThreadLocal的生命周期和线程的生命周期绑定,使用完ThreadLocal后,需要调用remove()方法进行清理,避免内存泄漏。

    解决方法:在使用完ThreadLocal后,在合适的地方调用remove()方法清理资源,可以使用try-finally语句块确保清理操作的执行,或者使用ThreadLocal的initialValue()方法设置初始值,这样在线程结束后会自动清理。

    1
    2
    3
    4
    5
    6
    private static ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(() -> {
    return "initial value";
    });

    // 使用完ThreadLocal后,调用remove()方法清理
    threadLocal.remove();
    1. 共享资源问题:如果多个线程共享了同一个ThredLocal变量,可能会导致数据错误或不确定的结果。每个线程应持有自己的ThreadLocal变量实例。

      解决方法:对于需要在多个线程之间共享变量的情况,应该创建多个ThreadLocal实例,每个线程持有自己的实例。

    2. 线程池使用时注意:在使用线程池时,需要特别小心ThreadLocal的使用,避免由于线程的重用而导致ThreadLocal数据的混乱。

      解决方法:使用线程池时,应避免使用ThreadLocal变量或者在使用前后显式清理ThreadLocal变量,确保每次任务执行时ThreadLocal的状态是干净的。

  总的来说,使用ThreadLocal时需要注意其生命周期、清理和共享的问题,合理使用并及时清理ThreadLocal,可以避免潜在的问题发生。

总结:ThreadLocal是一种Java技术,它允许在多线程环境中维护线程私有的变量副本。底层实现会使用一个类似于Map的结构来存储每个线程的变量副本。ThreadLocal并不是强引用或弱引用,它使用弱引用作为键来维护各个线程的变量副本,但变量本身由线程强引用。在使用ThreadLocal时,可能会出现内存泄漏的问题。如果线程结束了,但ThreadLocal中的变量没有被手动清理,那么该变量会一直存在于ThreadLocal的Map中,导致内存泄漏。解决这个问题的常见方式是在使用完ThreadLocal后调用remove()方法将变量从ThreadLocal中移除,或者使用Java 8中的ThreadLocal的InitialValue方法来提供默认值。另外,也可以使用ThreadLocal的弱引用方式来解决内存泄漏问题,例如使用InheritableThreadLocal。

十一、悲观锁和乐观锁的区别

悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。

锁实现:可以使用关键字synchronized、接口Lock的实现类

适用的场景:写操作较多的,先加锁可以保证写操作时数据正确

乐观锁:认为自己使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据

锁实现1:CAS算法, CAS即Compare And Swap,是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则返回false,不进行任何操作;例如ActomicInteger类的原子自增是通过CAS自选实现。

锁实现2:版本号控制:数据表中加上版本号字段 version,表示数据被修改的次数。当数据被修改时,这个字段值会加1,提交必须满足“ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略

适用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅提升