1.线程池都有哪些状态?
线程池的状态有RUNNING(运行中)、SHUTDOWN(关闭中,不接受新任务)、 STOP(立即关闭,中断正在执行任务的线程)和TERMINATED(终止)。
1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
2.线程池中 submit()和 execute()方法有什么区别?
submit()方法可以提交Callable任务,并返回Future对象;execute()方法只能提交 Runnable任务,无返回值。
submit()
和execute()
方法都是用于在线程池中提交任务的方法,但它们之间的主要区别在于任务的返回值和异常处理。
execute()
方法是用于提交不需要返回值的任务。它接收一个Runnable对象作为参数,然后在线程池中执行这个Runnable对象。execute()
方法没有返回值,因此不能用来获取任务的结果。如果任务执行过程中出现异常,execute()
方法会直接抛出这个异常,而不会返回任何错误信息。
submit()
方法则可以用于提交需要返回值的任务。它接收一个Callable对象作为参数,然后在线程池中执行这个Callable对象。submit()
方法返回一个Future对象,可以通过这个Future对象来获取任务的结果。如果任务执行过程中出现异常,Future对象会保存这个异常,在获取任务结果时抛出。
总结一下,如果你需要获取任务的结果,那么应该使用submit()
方法;如果你不需要获取任务的结果,或者你不需要等待任务执行完成,那么可以使用execute()
方法。
3.在 java 程序中怎么保证多线程的运行安全?
可以使用synchronized关键字、Lock接口、原子类等机制,确保多个线程访问共享资源时 不会出现数据竞争。
在Java程序中,有多种方法可以保证多线程的运行安全。以下是一些主要的策略:
- 同步(Synchronization): Java的
synchronized
关键字可以保证同一时间只有一个线程可以执行某个方法或者某个代码块,这样就可以避免多个线程同时修改同一个数据而引发的不一致问题。 - 使用
volatile
关键字:volatile
关键字可以保证变量的修改对所有线程可见,避免了线程之间的数据不一致问题。 - 使用
Atomic
类:Java的java.util.concurrent.atomic
包提供了一些原子操作类,如AtomicInteger
、AtomicLong
等,这些类可以保证对基本数据类型的操作是原子的。 - 使用线程安全的数据结构:Java提供了一些线程安全的数据结构,如
Vector
、Hashtable
等,这些数据结构内部已经实现了同步,可以保证在多线程环境下的安全。 - 使用并发包(java.util.concurrent):Java的并发包提供了一些高级的并发工具,如
Semaphore
、CountDownLatch
、CyclicBarrier
等,这些工具可以帮助你更好地控制线程的执行流程,避免多个线程同时访问共享资源的问题。 - 避免共享状态:尽可能地设计出无状态的程序,或者将状态信息局部化,可以减少线程间的竞争。如果必须共享状态,那么应该使用上述提到的方法来保证线程安全。
- 使用高级的并发类:Java 5以后,提供了一些更高级的并发类,如
ThreadPoolExecutor
、ScheduledThreadPoolExecutor
等,它们可以更灵活地控制线程池的大小和任务的执行。 - 避免死锁:死锁是多线程编程中常见的问题,可以通过避免循环等待,按顺序获取锁,或者使用Java的
ReentrantLock
等机制来避免。
以上这些方法都可以在一定程度上保证多线程的运行安全,但需要注意的是,没有一种方法可以解决所有的并发问题。在实际编程中,需要根据具体的问题和场景来选择合适的方法。
4.多线程锁的升级原理是什么?
在JVM中,锁会根据竞争情况从无锁升级为偏向锁、轻量级锁,最终升级为重量级锁,以适 应不同场景的线程竞争。
多线程锁的升级原理主要是指在Java中,当一个线程需要访问共享数据时,会先尝试获取锁,如果成功,则进入临界区执行操作;如果失败,则说明存在竞争,需要进行锁升级。
具体的锁升级过程如下:
- 在偏向锁状态下,对象头中的Mark Word被设置为偏向锁标记,并记录了持有锁的线程ID。因此,当一个线程访问共享数据时,无需进行同步操作,可以直接进入临界区执行操作。如果其他线程也需要访问该共享数据,此时需要升级为轻量级锁状态。
- 在轻量级锁状态下,对象头中的Mark Word被设置为指向锁记录的指针,同时锁记录结构体中包含了持有锁的线程ID和锁标志位等信息。此时,竞争线程会使用CAS(Compare and Swap)操作尝试获取锁,如果成功获取锁,则直接进入临界区执行操作;如果获取失败,则说明存在竞争,需要升级为重量级锁状态。
- 在重量级锁状态下,对象头中的Mark Word被设置为重量级锁标记,并将当前线程挂起,等待锁被释放后再唤醒线程进行竞争。由于重量级锁采用了操作系统内核的互斥机制,因此会引入较大的性能开销。
5.什么是死锁?
死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,他们都将无法推进下去,陷入永久等待状态,这种现象称为死锁。
6.怎么防止死锁?
可以使用避免加锁顺序破坏、使用定时锁等方法来避免死锁的发生。
死锁是多线程编程中常见的问题,可以采取以下几种方法来预防和避免:
- 避免使用共享资源:尽量避免使用共享资源,因为共享资源是导致死锁的主要原因之一。如果必须使用共享资源,可以考虑使用锁或者信号量来进行同步。
- 按照顺序获取锁:当多个线程需要获取多个锁时,应该按照一定的顺序获取锁,避免出现循环等待的情况。例如,线程A获取锁1后才能获取锁2,而线程B获取锁2后才能获取锁1,这种情况容易导致死锁。
- 使用定时锁:对于需要长时间持有锁的情况,可以考虑使用定时锁。定时锁可以在一定时间后自动释放锁,避免长时间持有锁导致死锁的情况。
- 使用锁的分级管理:将不同的锁分为不同的级别,高级别的锁可以获得更多的资源,低级别的锁只能获得少量的资源。当低级别的锁被占用时,只有等待高级别的锁被释放后才能继续执行。
- 使用死锁避免算法:死锁避免算法是一种预防死锁的算法,它通过限制线程的行为来避免死锁的发生。例如,银行家算法是一种常用的死锁避免算法,它通过检查线程的请求是否会导致死锁来决定是否满足线程的请求。
- 使用锁的粒度更细:将锁的粒度变得更细,可以减少线程之间的竞争,降低死锁的概率。例如,使用多个独立的锁来保护不同的资源,而不是使用一个全局的锁。
- 避免线程饥饿:当一个线程长时间无法获取到需要的资源时,可能会导致死锁的发生。因此,应该尽量避免线程饥饿的情况,合理分配资源给不同的线程。
7.ThreadLocal 是什么?有哪些使用场景?
ThreadLocal是一种线程本地变量,每个线程都拥有自己的变量副本,常用于实现线程封闭 和线程上下文信息传递。
ThreadLocal 是一个Java的类,它用于创建线程局部变量。线程局部变量是每个线程自己独有的变量,它不会在线程之间共享。ThreadLocal的实例通常是在类中以静态字段的方式存在的。
ThreadLocal 的使用场景主要有以下几类:
- 方便同一个线程使用某一对象,避免不必要的参数传递。
- 线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响)。
- 获取数据库连接、Session、关联ID等。例如在 Spring 的事务管理器中,会通过 ThreadLocal 存储事务对象,以保证每个线程都在各自的 Connection 上进行数据库操作,避免出现线程安全问题。
然而,需要注意的是,在管理环境下(如 Web 服务器)使用 ThreadLocal 时要特别小心。因为工作线程的生命周期比任何应用变量的生命周期都要长,如果 ThreadLocal 变量在工作完成后没有得到释放,可能会导致内存泄露的风险。
8.说一下 synchronized 底层实现原理?
synchronized使用了对象的内部锁(监视器锁),它可以用来修饰代码块或方法,保证在 同一时刻只有一个线程可以进入临界区。
synchronized 是 Java 语言中的关键字,用于实现同步锁。它的底层实现原理是通过对象内部的一个叫做监视器锁(monitor)来实现的,而监视器锁本质又是依赖于底层的操作系统的 MutexLock(互斥锁)来实现的。
当一个线程要执行一个 synchronized 方法时,它需要先获得锁才能执行该方法。如果该线程已经获得了锁,则可以直接执行该方法;否则,该线程会被挂起,等待其他线程释放锁。一旦有线程释放了锁,就会唤醒等待的线程,让其获得锁并执行。
synchronized 底层实现原理还包括对代码块的加锁和解锁。当线程执行到 synchronized 代码块时,需要先获得锁才能执行后面的代码块。当线程执行到 synchronized 代码块的末尾时,会自动释放锁。这样可以保证同一时间只有一个线程可以执行该代码块。
每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。这个计数器的作用是防止死锁的发生。如果一个线程已经获得了多次锁,那么在下一次尝试获取锁时,该线程必须等待其他线程释放锁,否则就会发生死锁。
另外,synchronized 底层实现原理还涉及到对方法进行隐式的加锁和解锁。当线程要执行的方法被标注上 synchronized 时,需要先获得锁才能执行该方法。这个锁可以通过 ACC_SYNCHRONIZED 关键字来实现。当线程执行到 synchronized 方法时,会自动获得锁并执行该方法。当线程执行完 synchronized 方法后,会自动释放锁。
总之,synchronized 是 Java 语言中实现同步的关键字之一,它的底层实现原理是通过监视器锁和 MutexLock 来实现的。通过对方法进行隐式的加锁和解锁、对代码块进行加锁和解锁以及对计数器的使用,可以有效地保证多线程程序的正确性和稳定性。
9.synchronized 和 volatile 的区别是什么?
synchronized是一种独占锁,可以实现原子操作和临界区的同步;volatile是一种轻量级的 同步机制,用于保证可见性和禁止指令重排序。
synchronized和volatile是Java中两种不同的线程同步机制,它们有以下区别:
- 作用机制:synchronized是在方法或代码块前加上同步锁,保证同一时刻只有一个线程可以执行该段代码。而volatile关键字则用于保证多线程对变量的访问一致性,它不会阻塞线程,而是通过在内存和CPU之间建立缓存一致性协议来保证变量的可见性。
- 锁的粒度:synchronized可以修饰方法或代码块,而volatile只能修饰变量。
- 内存语义:synchronized可以保证被修饰的方法或代码块在每个线程中的执行是按照顺序进行的,不会出现数据不一致的问题。而volatile则无法保证这一点。
- 性能:使用synchronized会带来额外的开销,因为它需要进行线程阻塞和唤醒的操作。而volatile则不需要进行这些操作,因此性能相对较好。
- 原子性:synchronized可以保证被修饰的方法或代码块具有原子性,即这些代码不可分割。而volatile无法保证原子性。
综上所述,synchronized和volatile各有优缺点,需要根据具体的使用场景来选择合适的同步机制。
10.synchronized 和 Lock 有什么区别?
synchronized是Java内置的关键字,自动管理锁的获取和释放;Lock是Lock接口的实现 类,需要手动管理锁的获取和释放,提供了更灵活的锁控制。
synchronized和Lock是Java中两种不同的线程同步机制,它们有以下区别:
- 关键字和接口的区别:synchronized是一个关键字,而Lock是一个接口。
- 加锁方式:synchronized是隐式的加锁,而Lock是显式的加锁。
- 作用范围:synchronized可以作用于方法上或者代码块上,而Lock只能作用于代码块上。
- 底层实现:synchronized底层使用的是objectMonitor,而Lock底层使用的是AQS。
- 支持的锁类型:synchronized是非公平锁,而Lock可以是公平锁也可以是非公平锁。
- 超时机制:synchronized没有超时机制,而Lock中的trylock可以支持超时机制。
- 可中断性:synchronized不可中断,而Lock中的lockInterruptibly可中断的获取锁。
- 等待和唤醒机制:synchronized使用object类的wait和notify进行等待和唤醒,而Lock使用condition接口进行等待和唤醒(await和signal)。
- 个性化定制:Lock支持个性化定制,使用了模板方法模式,可以自行实现lock方法。
综上所述,synchronized和Lock都是Java中实现线程同步的机制,它们各有优缺点,需要根据具体的使用场景来选择合适的同步机制。
11.synchronized 和 ReentrantLock 区别是什么?
synchronized是关键字,无法中断等待获取锁的线程;ReentrantLock是Lock接口的实现 类,可以中断等待获取锁的线程。
synchronized 和 ReentrantLock 是Java中两种不同的线程同步机制,它们有以下区别:
- 关键字和接口的区别:synchronized是一个关键字,而ReentrantLock是一个接口。
- 加锁方式:synchronized是隐式的加锁,而ReentrantLock需要显式地调用lock()和unlock()方法来加锁和解锁。
- 锁的释放:synchronized在Java编译器下自动释放锁,而ReentrantLock需要程序员手动释放锁。
- 公平性:synchronized不具有公平性,而ReentrantLock可以通过构造函数来设置公平性。
- 锁状态查询:ReentrantLock提供了一个isHeldByCurrentThread()方法可以查询当前线程是否持有锁,而synchronized则没有这个功能。
- 锁升级和降级:synchronized不支持锁升级和降级,而ReentrantLock可以通过Condition接口来实现锁升级和降级。
- 等待可中断性:synchronized不支持等待可中断性,而ReentrantLock可以通过Condition接口来实现等待可中断性。
- 锁的可重入性:synchronized和ReentrantLock都支持可重入锁。
- 锁的粒度:synchronized可以作用于方法或代码块上,而ReentrantLock只可以作用于代码块上。
综上所述,synchronized和ReentrantLock都是Java中实现线程同步的机制,它们各有优缺点,需要根据具体的使用场景来选择合适的同步机制。
22.说一下 atomic 的原理?
atomic包提供了一些原子操作类,通过CAS(Compare and Swap)操作实现了多线程环 境下的线程安全,确保操作的原子性
Atomic(原子性)是指一个操作是不可中断的,即使在多线程环境下也不会被其他线程干扰。在并发编程中,原子性是保证数据一致性的关键。
Atomic操作的实现原理主要包括原子指令、自旋锁、读写锁和原子变量等。原子指令可以保证指令执行的不可中断性,自旋锁可以通过不断循环来等待锁的释放,读写锁可以实现对共享资源的并发访问,原子变量则可以保证对共享数据的原子性操作。
在实现Atomic时,需要保证操作的数据是共享的,并且需要保证对数据的访问是原子的。因此,需要使用锁或者其他同步机制来保证操作的原子性和可见性。同时,还需要考虑操作的具体实现方式,例如使用硬件级别的原子指令、使用自旋锁、读写锁或者使用原子变量等。
总之,Atomic的实现原理是通过保证操作的原子性和可见性来保证数据的一致性和可靠性。