一、并行和并发有什么区别?
并行是指两个或多个事件在同一时刻发生,是在不同实体上的多个事件;
并发是指两个或多个事件在同一时间间隔发生,是在同一个实体上的多个事件
二、线程和进程的区别?
线程和进程是操作系统中的两个基本概念,它们在执行程序时扮演不同的角色。进程是操作系统进行资源分配和调度的独立单位,每个进程拥有自己的内存空间和系统资源。相比之下,线程是进程中的一个实体,是CPU调度和执行的更小单位,同一进程的多个线程共享相同的内存空间和资源。
在资源管理方面,每个进程拥有独立的地址空间,而线程共享它们所属进程的资源。创建进程的开销比创建线程要大,因为进程需要独立的内存和系统资源。通信方面,进程间通信需要特定的机制,而线程间可以直接读写共享内存进行通信。
在实际应用中,如果我们需要提高应用程序的响应性或利用多核处理器的优势,我们可能会使用多线程。而当我们需要运行完全隔离的应用程序,或者需要确保一个应用程序的崩溃不会影响到另一个应用程序时,我们可能会选择多进程。
总的来说,选择使用线程还是进程取决于具体的应用场景和性能要求。
三、守护线程是什么?
在java线程开发中,有两种线程:User Thread(用户线程);Daemon Thread(守护线程)
普通用户进程在JVM退出时依然会继续执行,导致JVM并不能退出
普通进程可以使用setDaemon(true)方法升级为守护进程,守护进程在JVM退出时会自动结束运行。
守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
四、创建线程有哪几种方式?
继承Thread类并实现run方法,调用继承类的start方法开启线程;
通过实现Runnable接口,重写run方法,调用线程对象的start方法开启线程;
除此之外,还可以通过实现Callable接口,实现call方法,并用FutureTask类包装Callable对象开启线程。
五、说一下 runnable 和 callable 有什么区别?
在Java编程中,Runnable和Callable都是用于多线程编程的接口,但它们之间存在一些关键的区别:
- 方法不同:
- Runnable接口只有一个
run()
方法,该方法不返回任何值,因此无法抛出任何checked Exception(即需要显式捕获或声明的异常)。 - Callable接口则有一个
call()
方法,它可以返回一个值,并且可以抛出一个checked Exception。
- Runnable接口只有一个
- 返回值不同:
- Runnable的
run()
方法没有返回值,只是一个void类型的方法。 - Callable的
call()
方法必须有一个返回值,并且返回值的类型可以通过泛型进行指定。
- Runnable的
- 异常处理不同:
- 在Runnable中,我们无法对
run()
方法抛出的异常进行任何处理,这些异常通常需要由调用线程来处理。 - 而Callable的
call()
方法抛出的异常可以直接被捕获并处理。
- 在Runnable中,我们无法对
- 使用场景不同:
- Runnable适用于那些不需要返回值,且不会抛出checked Exception的情况,比如简单的打印输出或者修改一些共享的变量。
- Callable则适用于需要返回结果并且可能抛出checked Exception的复杂计算任务。
- 实现方式:
- Runnable通常通过继承Thread类或者实现Runnable接口(但不继承Thread类)来实现多线程。
- Callable则必须实现Callable接口,并且通常与ExecutorService和Future一起使用,以便获取任务的结果和处理可能的异常。
总的来说,Runnable和Callable的主要区别在于它们的方法、返回值、异常处理和使用场景。选择使用哪个接口取决于你的具体需求。
六、线程有哪些状态?
新建、就绪、运行、阻塞、死亡
七、sleep() 和 wait() 有什么区别?
sleep()
和 wait()
都是Java中用于控制线程状态的方法,但它们之间存在显著的区别。
- 所属类和方法类型:
sleep()
是Java中Thread
类的静态方法。wait()
是Java中Object
类的成员方法,所有Java对象都继承了此方法。
- 锁的释放:
sleep()
方法使当前线程进入睡眠状态(暂停执行)指定的时间,但不会释放任何锁。这意味着如果线程在调用sleep()
之前已经持有了某个对象的锁,那么其他线程仍然无法访问该对象,直到睡眠的线程醒来并继续执行。wait()
方法则使当前线程进入等待状态,并释放它所持有的对象的锁。这使得其他线程有机会获取该对象的锁并执行。当其他线程调用同一对象的notify()
或notifyAll()
方法时,等待的线程可以被唤醒并重新获取锁。
- 唤醒方式:
sleep()
方法在指定的时间结束后自动唤醒线程,无需外部干预。wait()
方法则依赖于其他线程调用同一对象的notify()
或notifyAll()
方法来唤醒。
- 异常处理:
sleep()
方法在调用时可能会抛出InterruptedException
异常,该异常是检查型异常,需要在代码中显式处理。wait()
方法在调用时不会抛出InterruptedException
异常,但在等待过程中如果线程被中断,那么在返回时会清除中断状态(即将中断标志位设置为false)。如果需要检查线程是否被中断,可以在等待后使用Thread.interrupted()
方法进行检查。
- 使用场景:
sleep()
方法通常用于简单的线程休眠或延迟执行,例如实现定时任务或控制线程的执行频率。wait()
和notify()
/notifyAll()
方法则更多地用于实现线程间的通信和同步,例如生产者-消费者模型中的线程协作。
需要注意的是,由于 wait()
和 notify()
/notifyAll()
方法与对象的锁紧密相关,因此在使用这些方法时需要特别注意锁的获取和释放,以避免出现死锁或其他同步问题。同时,由于 sleep()
方法不会释放锁,因此在使用时需要谨慎考虑其可能对其他线程产生的影响。
八、notify()和 notifyAll()有什么区别?
notify()方法会唤醒对象等待池中的一个线程,进入锁池;
notifyAll()方法会唤醒等待池中的所有线程,进入锁池。
九、线程的 run()和 start()有什么区别?
在Java中,线程的run()
和start()
方法之间存在关键的区别。
- 方法的作用:
run()
:这是Runnable
接口中定义的方法,也是Thread
类中的一个方法。当你直接调用一个线程的run()
方法时,你实际上是在当前线程(即调用run()
方法的线程)中执行该方法的代码,而不是创建一个新的线程来执行它。因此,run()
方法的调用是同步的,并不会导致新的线程启动。start()
:这是Thread
类中的方法,用于启动一个新线程来执行该线程的run()
方法。当你调用一个线程的start()
方法时,Java虚拟机(JVM)会为该线程分配必要的系统资源,并调度一个新的线程来执行该线程的run()
方法。这意味着run()
方法的代码会在新的线程中异步执行,而不是在调用start()
方法的线程中同步执行。
- 线程状态:
- 当你创建一个新的
Thread
对象并调用其start()
方法时,该线程的状态会从NEW
(新建)变为RUNNABLE
(可运行),并最终变为RUNNING
(运行)或BLOCKED
(阻塞)等其他状态。 - 如果你直接调用线程的
run()
方法,那么它只会在当前线程中执行,而不会改变任何线程的状态。
- 当你创建一个新的
- 异常处理:
- 如果在
run()
方法中抛出了未检查的异常(例如RuntimeException
),那么该异常将由调用start()
方法创建的线程捕获并处理(如果线程中有相应的异常处理代码)。然而,如果直接调用run()
方法并在其中抛出异常,那么该异常将由调用run()
方法的当前线程捕获并处理。
- 如果在
- 使用场景:
- 通常,你应该总是调用线程的
start()
方法来启动一个新线程并执行其run()
方法中的代码。直接调用run()
方法通常是不正确的做法,因为它不会创建新的线程,而是直接在当前线程中同步执行代码。
- 通常,你应该总是调用线程的
总结来说,run()
方法是线程要执行的代码所在的地方,而start()
方法则用于启动一个新线程来执行该线程的run()
方法。你应该始终调用线程的start()
方法来启动线程,而不是直接调用其run()
方法。
十、创建线程池有哪几种方式?
主要使用Excutors提供的通用线程池创建方法,去创建不同配置的线程池
newCachedThreadPool();特点:用来处理大量短时间工作任务的线程池
newFixedThreadPool(int nThreads);特点:重用指定数目(nThreads)的线程
newSingleThreadExecutor();特点:工作线程数目限制为1
newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)可以进行周期或定时性的工作调度
newWorkStealingPool(int parallelism);特点:JDK8以后才加入
十一、线程池都有哪些状态?
在Java中,线程池的状态主要包括以下几种:
- RUNNING(运行):线程池处于正常运行状态。当线程池被创建后,它会自动进入此状态。除非手动调用关闭方法,否则线程池在整个程序运行期间都会保持此状态。
- SHUTDOWN(关闭):线程池正在关闭状态。当调用
shutdown()
方法时,线程池会切换到此状态。在此状态下,线程池不再接受新任务,但会继续处理已经提交的任务。 - STOP(停止):线程池停止状态。当调用
shutdownNow()
方法时,线程池会切换到此状态。在此状态下,线程池不仅不接受新任务,还会尝试停止所有正在执行的任务,并忽略任务队列中已有的任务。 - TIDYING(整理):线程池正在整理状态。当所有的任务都已终止(包括任务队列中的任务),并且工作线程数量为零时,线程池会进入此状态。在此状态下,会执行线程池的钩子函数
terminated()
。 - TERMINATED(终止):线程池已经终止状态。在线程池完成整理工作(即执行完
terminated()
方法后),它会切换到此状态。
十二、线程池中 submit()和 execute()方法有什么区别?
execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
execute() 没有返回值;而 submit() 有返回值
submit()的返回值Future调用get方法时,可以捕获处理异常
十三、在 java 程序中怎么保证多线程的运行安全?
在Java程序中,保证多线程的运行安全通常涉及多个方面,包括同步、锁、线程局部变量、原子类、并发集合以及线程安全的设计模式等。以下是一些关键的策略和方法:
- 同步(Synchronization):
- 使用
synchronized
关键字来确保同一时间只有一个线程可以执行某个代码块或方法。 synchronized
可以修饰方法或代码块。- 同步块可以指定一个对象作为锁,只有持有该锁的线程才能进入同步块。
- 使用
- 锁(Locks):
- Java 5 引入了
java.util.concurrent.locks
包,提供了更灵活的锁机制,如ReentrantLock
、ReadWriteLock
等。 - 这些锁提供了更细粒度的控制,比如可重入锁、尝试锁、公平锁等。
- Java 5 引入了
- 线程局部变量(ThreadLocal):
ThreadLocal
用于创建线程局部变量,每个线程都有自己独立的变量副本,互不影响。- 这对于防止多个线程之间共享可变状态是非常有用的。
- 原子类(Atomic Classes):
- Java的
java.util.concurrent.atomic
包提供了一组原子类,如AtomicInteger
、AtomicLong
等。 - 这些类提供了原子操作,如自增、自减等,可以确保多线程环境下这些操作的安全。
- Java的
- 并发集合(Concurrent Collections):
- Java的并发包(
java.util.concurrent
)提供了一系列线程安全的集合类,如ConcurrentHashMap
、CopyOnWriteArrayList
等。 - 这些集合类内部实现了复杂的同步机制,以确保在多线程环境下的正确性和性能。
- Java的并发包(
- 避免共享可变状态:
- 尽可能减少共享的可变状态。如果数据可以在方法或线程内局部使用,就不要在多个线程间共享。
- 优先使用不可变对象(Immutable Objects)。不可变对象一旦被创建,其内容就不能被改变,因此它们总是线程安全的。
- 使用线程安全的设计模式:
- 如生产者-消费者模式(Producer-Consumer Pattern)、读者-写者模式(Reader-Writer Pattern)等。
- 这些设计模式提供了一套用于多线程交互的框架和策略。
- 减少线程间的交互:
- 尽量将工作分解为可以独立执行的任务,并避免任务之间的过度交互。
- 使用线程池来管理和复用线程,以减少线程创建和销毁的开销。
- 使用Future和Callable:
Future
和Callable
可以与ExecutorService
一起使用,以实现异步执行和获取结果的功能。- 这有助于减少线程间的直接交互,提高程序的响应性和吞吐量。
- 监控和调试:
- 使用Java的并发工具(如
jstack
、jvisualvm
等)来监控和调试多线程程序。 - 这些工具可以帮助你发现死锁、竞态条件等并发问题,并提供相应的解决方案。
- 使用Java的并发工具(如
十四、多线程锁的升级原理是什么?
锁的级别:无锁 => 偏向锁 => 轻量级锁 => 重量级锁
无锁:没有对资源进行锁定,所有线程都可以访问,但是只有一个能修改成功,其他的线程会不断尝试,直至修改成功。
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,偏向锁,指的就是偏向第一个加锁线程,该线程不会主动释放偏向锁,只有当其他线程尝试竞争偏向锁时才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
十五、什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用,这些进程都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁的规范定义是:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。一种情形是,此时执行程序中两个或多个进程发生永久堵塞(等待),每个进程都在等待被其他进程占用并堵塞了的资源。例如,如果进程A锁住了记录1并等待记录2,而进程B锁住了记录2并等待记录1,这样两个进程就发生了死锁现象。
在计算机系统中,如果系统的资源分配策略不当,或者程序员写的程序有错误等,都可能导致进程因竞争资源不当而产生死锁的现象。为了避免死锁,可以采取一些预防措施,如资源按序申请、避免嵌套锁、使用定时锁等。此外,对于已经发生的死锁,也可以采用一些方法来进行检测和解除,如资源分配图法、银行家算法等。
十六、怎么防止死锁?
预防:资源一次性分配;可剥夺资源;资源有序分配;超时放弃
避免:银行家算法;
检测:为每个进程和每个资源建立唯一的ID,建立资源分配表和进程等待表
解除:剥夺资源;撤销进程
十七、ThreadLocal 是什么?有哪些使用场景?
ThreadLocal
是 Java 提供的一个类,它提供了线程局部变量。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己的独立初始化的变量副本。ThreadLocal
实例通常用作私有静态字段,在类中被声明为 private static final
。
以下是 ThreadLocal
的一些主要特点和使用场景:
特点
- 线程隔离:每个线程都持有一个对该
ThreadLocal
变量的隐式引用,并访问自己持有的变量的副本,从而实现了线程之间的数据隔离。 - 避免参数传递:在复杂的方法调用中,如果需要传递多个状态变量,可以通过
ThreadLocal
来避免显式的参数传递,从而提高代码的可读性和可维护性。 - 线程安全性:由于每个线程都有自己的变量副本,因此不需要额外的同步机制就可以保证线程安全。
使用场景
- 数据库连接管理:在 Web 应用中,每个请求都需要一个数据库连接。可以使用
ThreadLocal
来保存每个线程的数据库连接,从而避免在方法调用之间传递数据库连接对象。 - 用户会话管理:在 Web 应用中,每个用户都有一个会话,包含了一些用户的私有信息(如用户 ID、角色等)。可以使用
ThreadLocal
来保存这些信息,以便在请求处理过程中随时访问。 - 事务管理:在分布式系统中,事务管理通常涉及多个服务之间的协作。可以使用
ThreadLocal
来保存当前事务的上下文信息,如事务 ID、参与者列表等。 - 日志记录:在日志记录中,可能需要记录一些与当前线程相关的信息(如线程 ID、线程名称等)。可以使用
ThreadLocal
来保存这些信息,以便在日志记录过程中随时访问。 - 线程池中的线程状态:在使用线程池时,由于线程是复用的,因此不能在线程中直接保存状态信息。可以使用
ThreadLocal
来保存每个线程的状态信息,以便在任务执行过程中随时访问。
需要注意的是,虽然 ThreadLocal
可以解决一些线程之间数据共享的问题,但它也带来了内存泄漏的风险。因为 ThreadLocal
变量的生命周期与线程的生命周期相同,如果线程长时间不退出(如使用线程池时),那么 ThreadLocal
变量所引用的对象就可能一直不会被垃圾回收器回收,从而导致内存泄漏。因此,在使用 ThreadLocal
时,需要注意及时清理不再需要的变量。
十八、说一下 synchronized 底层实现原理?
同步代码块是通过monitorenter和monitorexit指令获取线程的执行权;
同步方法是通过加ACC_SYNCHRONIZED 标识实现线程的执行权的控制
十九、synchronized 和 volatile 的区别是什么?
- volatile本质是在告诉vm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronize则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
- volatile仅能实现变量的修改可见性,不能保证原子性;synchronize可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronize可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化,synchronize标记的变量可以被编译器优化
二十、synchronized 和 Lock 有什么区别?
synchronized是关键字,属于jvm层面;Lock是具体类,是api层面的锁;
synchronized无法获取锁的状态,Lock可以判断;
synchronized用于少量同步,Lock用于大量同步。
二十一、synchronized 和 ReentrantLock 区别是什么?
synchronized代码执行结束后线程自动释放对锁的占用;Reentrantlock需要手动释放锁;
synchronized不可中断,除非抛出异常或者执行完成;Reentrantlock可中断;
synchronize非公平锁;Reentrantlock默认非公平锁,也可公平锁;
ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized要么随机唤醒一个,要么唤醒全部线程。
二十二、理解乐观锁和悲观锁
乐观锁和悲观锁是两种在并发编程中常用的并发控制策略,它们的主要区别在于对并发冲突的处理方式和假设前提。
- 悲观锁:
- 悲观锁的核心思想是“悲观”,它认为在并发操作中,数据冲突的可能性很大。因此,在每次操作数据时,悲观锁都会先对数据进行加锁,以确保在数据被处理的过程中不会被其他线程修改。
- 悲观锁的实现方式通常是使用数据库的行锁、表锁、读锁、写锁等机制,或者在Java中使用
synchronized
关键字来实现。 - 悲观锁的优点是确保了数据的一致性和安全性,但由于它总是假设最坏的情况,因此可能会导致线程间的频繁阻塞和上下文切换,降低并发性能。
- 悲观锁适用于写操作较多的场景,如银行转账等需要确保数据一致性的场景。
- 乐观锁:
- 乐观锁的核心思想是“乐观”,它认为在并发操作中,数据冲突的可能性很小。因此,乐观锁不会直接对数据进行加锁,而是在数据更新时检查数据是否被其他线程修改过。
- 乐观锁的实现方式通常是在数据表中增加一个版本号或时间戳字段,在更新数据时检查该字段的值是否发生变化。如果版本号或时间戳没有变化,则说明数据没有被其他线程修改过,可以进行更新;如果版本号或时间戳发生了变化,则说明数据已经被其他线程修改过,此时需要回滚操作或重新读取数据。
- 乐观锁的优点是提高了并发性能,因为它不会阻塞其他线程对数据的访问。但是,如果数据冲突频繁发生,那么乐观锁可能会导致大量的回滚操作,降低系统性能。
- 乐观锁适用于读操作较多的场景,如电商网站的商品浏览、新闻网站的文章阅读等场景。在这些场景中,由于读操作远多于写操作,因此使用乐观锁可以提高系统的并发性能。
总的来说,乐观锁和悲观锁的选择取决于具体的应用场景和对并发性能的要求。在写操作较多的场景中,使用悲观锁可以确保数据的一致性和安全性;而在读操作较多的场景中,使用乐观锁可以提高系统的并发性能。