十 并发编程
1 Java 怎么保证多线程运行安全?
线程安全是程序设计中的术语,指某个方法在多线程环境中被调用时,能正确的处理多个线程中的共享变量,使程序正确执行。Java 中线程安全体现在以下三个方面:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作
可见性:一个线程对主内存的修改可以及时地被其他线程看到
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序
因此,只要满足上述三个条件,我们就可以说该代码是线程安全的。那么,Java 中提供了如下解决方案:
使用 sychronized 关键字
使用线程安全类,如:java.util.concurrent 包下的类
使用并发包下 Lock 相关锁
总结:想要代码满足线程安全,只需要代码满足原子性、可见性、有序性即可。
2 线程和进程的区别?Java 实现的多线程的方式有哪几种?
线程和进程的区别: 进程是程序的一次动态执行过程,每个进程都有自己独立的内存空间。一个应用程序可以同时启动多个进程(比如浏览器可以开多个窗口,每个窗口就是一个进程)多进程操作系统能够运行多个进程,每个进程都能够循环利用所需要的 CPU 时间片,使的所有进程看上去像在同时运行一样。
线程是进程的一个执行流程,一个进程可以由多个线程组成,也就是一个进程可以同时运行多个不同的线程,每个线程完成不同的任务。
线程的并发运行:就是一个进程内若干个线程同时运行。(比如:word 的拼写检查功能和首字母自动大写功能是 word 进程中的线程)线程和进程的关系是一个局部和整体的关系,每个进程都由操作系统分配独立的内存地址空间,而同一进程的所有线程都在同一地址空间工作。
多线程实现方式: Java 多线程实现方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口通过FutureTask 包装器来创建 Thread 线程、使用 ExecutorService、Callable、Future 实现有返回结果的多线程。
继承 Thread 类,重写 run 方法
实现 Runnable 接口,重写 run 方法,实现 Runnable 接口的实现类的实例对象作为 Thread 构造函数的 target
通过 Callable 和 FutureTask 创建线程
通过线程池创建线程
3 线程有哪些基本状态,并描述每种状态
新建状态(New):当线程对象被创建时,它进入新建状态。此时,线程只是被创建,但还没有开始执行,也没有分配CPU时间,它只是一个尚未执行的任务。
就绪状态(Ready或Runnable):当线程调用了start()方法后,它进入就绪状态。这意味着线程已经准备好运行,但还需要等待操作系统的调度,以获取CPU时间片。
运行状态(Running):当线程被CPU选中并执行时,它进入运行状态。此时,线程正在执行其任务。
阻塞状态(Blocked):当线程因为某种原因(例如等待I/O操作完成或进入sleep状态)无法继续执行时,它进入阻塞状态。在阻塞状态中,线程暂时放弃CPU的使用权,直到某种条件满足(例如I/O操作完成或sleep时间结束),线程重新进入就绪状态,等待CPU的调度。
等待状态(Waiting):当线程需要等待其他线程执行完毕或者满足某个条件时,它会进入等待状态。例如,使用join方法时,当前线程会等待其他线程执行完毕。等待状态是线程主动放弃CPU使用权的一种形式。
超时等待状态(Timed Waiting):这是等待状态的一种特殊形式。当线程使用sleep方法时,它会进入一个有时限的等待状态。如果超过了设定的时间,线程会自动醒来并进入就绪状态。
终止状态(Terminated):当线程完成任务或被强制终止时,它进入终止状态。此时,线程释放所有资源,并且不再占用CPU时间。一旦线程进入终止状态,就不能再复生。
4 同步和异步的区别
同步和异步是描述两个或多个操作之间如何相互关联和依赖的术语,特别是在编程和并发处理中。以下是同步和异步之间的主要区别:
同步(Synchronous)同步操作意味着两个或多个操作按照特定的顺序一个接一个地执行,后面的操作需要等待前面的操作完成。有顺序性:操作按照预定义的顺序执行。阻塞性:如果某个操作需要花费一些时间来完成(例如,I/O操作),那么后续的操作将被阻塞,直到该操作完成。可预测性:因为操作按照预定的顺序执行,所以程序的行为通常更容易预测。在传统的函数调用中,调用者等待函数执行完毕并返回结果,然后才能继续执行后续的代码。
异步(Asynchronous)异步操作允许两个或多个操作同时发生,不需要等待前一个操作完成。有并发性:操作可以同时开始,不需要等待其他操作完成。非阻塞性:一个操作的开始或结束不会阻塞其他操作的执行。不可预测性:由于操作的执行顺序和完成时间可能不确定,所以程序的行为可能更难以预测。异步I/O操作:当程序发起一个I/O请求(如读取文件或发送网络请求)时,它不需要等待操作完成就可以继续执行其他任务。当I/O操作完成时,程序会通过某种机制(如回调函数、Promise、Future或异步/等待语法)得到通知。
同步与异步的比较
性能:异步操作通常可以提高性能,因为在等待一个操作完成时,程序可以继续执行其他任务,从而充分利用资源。
复杂性:异步编程通常比同步编程更复杂,因为需要处理操作的完成顺序、错误处理以及状态管理等问题。
使用场景:同步操作适用于那些需要按照特定顺序执行且不需要等待的操作,例如简单的数学计算或内存操作。异步操作则适用于那些可能需要花费较长时间才能完成的操作,例如I/O操作、网络请求或长时间的计算任务。
总的来说,同步和异步是处理操作顺序和依赖关系的不同方式,每种方式都有其适用场景和优缺点。在选择使用同步还是异步时,需要根据具体的应用需求、性能要求和编程复杂性进行权衡。
5 并发与并行的区别
并发与并行是计算机科学中描述多个任务或操作如何同时执行的概念,并发和并行的主要区别在于任务或操作是否在同一时刻真正同时发生,以及它们对CPU资源的利用方式。并发是在宏观上同时执行多个任务,但在微观上仍然是顺序执行;而并行则是多个任务在同一时刻真正同时执行。
6 线程的 run()和 start()有什么区别
start() : 它的作用是启动一个新线程。通过 start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到 cpu 时间片,就开始执行相应线程的 run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,run 方法运行结束,此线程随即终止。start()不能被重复调用。用 start 方法来启动线程,真正实现了多线程运行,即无需等待某个线程的 run 方法体代码执行完毕就直接继续执行下面的代码。这里无需等待 run 方法执行完毕,即可继续执行下面的代码,即进行了线程切换。
run() : run()就和普通的成员方法一样,可以被重复调用。如果直接调用 run 方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run 方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
总结:调用 start 方法方可启动线程,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
7 runnable 和 callable 有什么区别
runnable 没有返回值,而实现 callable 接口的任务线程能返回执行结果;callable 接口实现类中的 run 方法允许异常向上抛出,可以在内部处理,try catch,但是 runnable接口实现类中 run 方法的异常必须在内部处理,不能抛出
8 什么是线程死锁
线程死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象。具体表现为,这些线程相互等待对方释放资源,但由于它们都持有对方所需的资源并不愿释放,导致了一个无解的循环等待状态。在这种情况下,若无外力作用,它们都将无法继续执行,系统因此处于死锁状态。死锁是多线程开发中应该坚决避免和杜绝的问题,因为它会导致程序无法正常终止,严重影响系统的性能和稳定性。
线程死锁通常由以下四个必要条件造成:
互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
解决线程死锁的方法主要是破坏这四个必要条件中的一个或多个。例如,可以通过确保线程在请求新资源前先释放已持有的资源,或者通过操作系统抢占某个线程的资源来打破循环等待等。
9 sleep() 方法和 wait() 方法区别和共同点
两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
两者都可以暂停线程的执行。 Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll()方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
10 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行
要确保线程 T2 在 T1 之后执行,并且 T3 在 T2 之后执行,可以使用线程同步机制。在 Java 中,可以使用 join() 方法来实现这种顺序执行。join() 方法的作用是让当前线程等待调用 join() 方法的线程执行完毕。下面是一个简单的 Java 示例代码:
public class ThreadOrdering { public static void main(String[] args) { Thread T1 = new Thread(() -> { // T1 的任务 System.out.println("T1 执行完毕"); }); Thread T2 = new Thread(() -> { try { // 等待 T1 执行完毕 T1.join(); } catch (InterruptedException e) { e.printStackTrace(); } // T2 的任务 System.out.println("T2 执行完毕"); }); Thread T3 = new Thread(() -> { try { // 等待 T2 执行完毕 T2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // T3 的任务 System.out.println("T3 执行完毕"); }); // 启动线程 T1.start(); T2.start(); T3.start(); }
}
11 volatile 关键字的作用
volatile关键字在编程中,特别是在多线程编程中,扮演着重要的角色。它的主要作用如下:
保证变量的可见性:当一个线程修改了一个由volatile修饰的变量的值,其他线程可以立即看到这个修改。这是因为volatile关键字禁止了指令重排序,从而确保共享变量的修改对所有线程都是可见的。
禁止指令重排序:编译器和处理器在编译和执行代码时,可能会对指令进行重排序以提高性能。然而,这种重排序有时会导致程序执行结果与预期不符。volatile关键字可以禁止这种重排序,保证程序按照预期的顺序执行,从而确保程序的正确性。
保证原子性(在特定情况下):volatile关键字可以保证一些简单的操作的原子性,例如++操作。但是,对于复合操作,volatile关键字无法保证原子性。在多线程编程中,对于需要保证原子性的复杂操作,通常需要使用synchronized关键字或其他同步机制。
需要注意的是,volatile关键字并不能完全替代其他的同步机制。在多线程编程中,应该根据具体情况选择使用volatile关键字或synchronized关键字等同步机制,以保证程序的正确性和效率。