一、进程和线程有什么区别?
进程和线程都是操作系统中用来实现多任务的概念。但是它们之间有一些重要的区别,如下所述:
-
(1)定义方面
进程:进程是操作系统中分配资源的基本单位,是正在运行中的一个程序。一个进程可以包含多个线程。每个进程有自己独立的地址空间,互相之间不能直接访问,必须通过进程间通信(IPC)来传递数据。
线程:线程是在进程内部运行的轻量级的任务单元。一个进程可以包含多个线程,线程共享进程的内存地址空间,可以访问同一组资源。在Java中,线程由Thread类来表示。 -
(2)资源开销方面
进程:由于每个进程都拥有自己独立的地址空间,因此在创建和撤销进程时需要耗费较高的系统开销。每个进程都需要独立的系统资源,如内存空间、文件句柄等,这些资源是由操作系统来分配和管理的。
线程:由于线程共享进程的地址空间,因此在创建和撤销线程时所需的系统开销较小。线程之间共享同一组资源,而不像进程那样需要复制一份资源,因此在资源管理方面有许多优势。 -
(3)并发性
进程:不同的进程之间相互独立,因此它们之间的并发性也很高。由于每个进程都有自己的独立地址空间,因此进程之间进行通信时需要使用IPC方式,这会带来一定的开销。
线程:由于线程共享进程的地址空间,因此线程之间的通信更加方便快捷。线程之间的切换也比进程之间的切换要快得多。 -
(3)可见性
进程:由于每个进程都有自己的独立地址空间,因此进程之间的数据是不能直接共享的。如果需要共享数据,则需要使用IPC方式。
线程:由于线程共享进程的地址空间,因此线程之间可以直接共享数据,而不需要进行复杂的通信操作。然而,由于缺乏同步机制,当多个线程同时访问共享变量时,可能会出现数据一致性问题。 -
(4)处理器分配
进程:进程是操作系统中分配CPU资源的基本单位。每个进程都拥有独立的CPU时间片,由操作系统来分配CPU时间片,决定每个进程何时执行。
线程:一个进程内的多个线程可以在同一时间内共享CPU时间片。由操作系统来分配
CPU时间片,决定哪个线程优先执行。
二、并发和并行有什么区别?
并发是指两个或者多个任务在同一时间间隔内被执行,它们之间可能存在交替执行的情况。在计算机领域,多任务操作系统通常使用轮询或者时间片等机制来实现并发执行。
例如,一个Web服务器可以同时处理多个客户端的请求,每个客户端的请求都能够得到相应的处理,因此看起来就像是这些任务在同时进行。但是实际上,这些任务并不是真正意义上的同时执行,而是通过对CPU资源的分配和切换来实现的。
并行是指两个或者多个任务在同一时刻实际上是同时执行的。在计算机领域,多核处理器和分布式计算系统是典型的并行计算系统,它们能够将不同的任务分配给不同的CPU或者计算节点来并行执行。
例如,图像处理软件可以将一个大的图像分割成多个小的部分,然后将这些部分分配给不同的CPU核心来并行处理,从而加速整个处理过程。
总的来说:并发是一种任务调度的方式,它能够让多个任务在同一时间间隔内并发执行;而并行则是一种任务同时执行的方式,它能够通过利用多个CPU核心或者计算节点来加速整个处理过程
三、介绍下多线程的实现方式?
多线程的实现方式主要有继承 Thread 类、实现 Runnable 接口、实现Callable 接口三种方法,其具体介绍如下:
- (1)继承 Thread 类
Java 中可以通过继承 Thread 类来创建线程,具体实现方式是定义一个类继承 Thread 类,重写 run() 方法,然后创建该类的对象,并调用 start() 方法启动线程。 - (2)实现 Runnable 接口
Java 中也可以通过实现 Runnable 接口来创建线程,具体实现方式是定义一个类实现 Runnable 接口,重写 run() 方法,然后创建 Thread 类的对象,并将该类的实例作为参数传递给 Thread 类的构造方法,最后调用 start() 方法启动线程。 - (3)使用 Callable 接口
Callable 接口是 Java 中用于实现带返回值的多线程任务的接口,可以用于执行需要返回结果的任务。具体实现方式是定义一个类实现 Callable 接口,重写 call() 方法,然后创建一个 FutureTask 对象,将该类的实例作为参数传递给 FutureTask 构造方法,最后创建一个线程,并将 FutureTask 对象作为参数传递给线程的构造方法,调用 start() 方法启动线程。
注:实现 Runnable 接口和 Callable 接口区别:Runnable接口不会返回结果或抛出检查异常,但是Callable接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用Runnable接口 。
四、 线程有哪些状态?生命周期是怎样的?
Java线程有以下几种状态:
- (1)NEW(新建):表示线程对象已经被创建,但是还没有被启动。
- (2)RUNNABLE(可运行/运行中):表示线程正在Java虚拟机中执行。在多线程中,RUNNABLE状态包括"就绪(READY)"和"运行(RUNNING)"两种状态。
- (3)BLOCKED(阻塞):表示线程已经被阻塞,因为它正在等待一个监视器锁(synchronized块)的释放,或者正在等待I/O操作完成等。
- (4)WAITING(无限期等待):表示线程正在等待其他线程执行任务,不会自动唤醒,需要其他线程调用notify()或notifyAll()方法才能唤醒该线程。
- (5)TIMED_WAITING(有限期等待):表示线程正在等待其他线程执行任务,并且等待时间有限,超过一定时间后会自动唤醒。
- (6)TERMINATED(终止):表示线程已经执行完毕,结束执行。
Java线程的生命周期大致如下:
- (1)新建(NEW):当创建了线程对象后,它便处于新建状态,此时它还没有开始运行。
- (2)就绪(RUNNABLE):当调用start()方法时,线程变成可运行状态,线程并没有真正的开始运行,只是表示可以运行,但没分配到CPU资源。
- (3)运行(RUNNING):当就绪状态的线程获得CPU资源时,便进入运行状态,开始执行run()方法的线程代码。
- (4)阻塞(BLOCKED):线程在执行过程中,可能被某些操作阻塞,如等待I/O输入输出、调用sleep()、wait()方法等情况,使得该线程暂时停止运行。当这些阻塞操作完成后,线程重新进入就绪状态。
- (5)等待(WAITING):线程通过调用 wait()方法或 join() 方法,在等待其他线程的通知或完成。
- (6)有限期等待(TIMED_WAITING):与WAITING类似,但是在等待一定的时间后会自动唤醒。
- (7)死亡(TERMINATED):当run()方法执行完毕,或者出现异常导致线程结束时,线程进入死亡状态。
五、Thread.sleep() 和 wait() 的区别?start() 和 run() 方法有什么区别?
Thread.sleep() 和 wait() 的区别:
- (1)作用对象不同:Thread.sleep() 是让当前线程休眠,等待一定的时间再执行;wait() 是让当前线程等待,直到其他线程通知或者等待超时。
- (2)调用方式不同:Thread.sleep()可以在任何地方使用,而wait()必须在synchronized代码块或方法内调用。
- (3)针对锁的处理不同:Thread.sleep()不会释放锁,而wait()在等待前需要释放占有的锁,当线程被唤醒后,会尝试重新获取锁。
- (4)使用目的不同:Thread.sleep()主要用于暂停当前线程,使其“睡眠”指定的时间,可以用来模拟耗时操作。wait()则是用于线程间通信,某个线程调用 wait() 方法后会进入等待状态,并释放占有的锁,等待其他线程通过notify()/notifyAll()方法来唤醒。
start() 和 run() 方法的区别:
- (1)start() 方法会启动一个新的线程,使该线程进入就绪状态,并在具备执行条件时执行 run() 方法;而调用 run() 方法,则只是简单的调用一个方法而已。
- (2)start() 方法是异步开始线程执行,程序继续向下执行,而不会阻塞当前线程;而调用 run() 方法,则是同步执行,会阻塞当前线程,直到 run() 方法执行完毕。
- (3)只有通过 start() 方法启动的线程才会被JVM视为一个单独的执行单元,才可以与其他线程并发执行;而如果直接调用 run() 方法,则只是在当前线程的上下文中依次执行,不会与其他线程并发执行。
总结:Thread.sleep() 和 wait() 的区别在于作用对象、调用方式、针对锁的处理和使用目的不同;start() 和 run() 方法的区别在于是否异步开始,是否阻塞当前线程以及是否与其他线程并发执行。
六、什么是线程死锁?该如何避免?
线程死锁指的是在多线程编程中,两个或多个线程相互等待对方释放资源的现象,从而导致程序无法继续执行下去,出现死循环的情况。
例如,线程A持有资源a并等待资源b,而线程B持有资源b并等待资源a,此时两个线程都无法继续执行下去,就会导致线程死锁。
线程死锁需要同时满足以下条件:
- (1)互斥条件:该资源任意一个时刻只由一个线程占用。
- (2)占有且申请条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- (3)不剥夺条件:线程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。
- (4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
既然需要同时满足上面四个条件才会出现死锁,那避免死锁的办法,就可以变成破坏其中一个或者多个条件就可以了。一般有如下方法:
- (1)破坏请求与保持条件 :一次性申请所有的资源。
- (2)破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- (3)破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
注:互斥条件无法破坏
七、什么是线程同步?该如何实现?
线程同步指的是多个线程在访问共享资源时,需要协调彼此的行为,以避免出现竞态条件(race condition)和死锁等问题,保证多个线程对共享资源的访问顺序是有序的、正确的。
例如:当两个人对一个银行账户进行取钱操作时,进入界面后,他们都可以看到余额有100元,如果不进行同步的话,则会发生两个人都能够取款100的现象,导致数据发生错误。
常见的线程同步实现方式如下:
- (1)互斥锁(mutex):通过对共享资源加锁,保证同一时刻只能有一个线程访问该资源,其他线程需要等待解锁才能继续访问。
- (2)信号量(semaphore):用于控制同时访问某个资源的线程数量。可以分为二元信号量和计数信号量两种类型。
- (3)条件变量(condition variable):用于在多个线程之间传递消息,当某个条件满足时,会通知等待在该条件变量上的线程继续执行。
- (4)读写锁(read-write lock):用于读写操作分离的场景,通过加锁的方式保证写操作的独占性,而读操作可以并发地进行。
八、介绍下synchronized和ReentrantLock的联系和区别?
synchronized和ReentrantLock都是Java语言中用于实现线程同步的机制。它们的联系和区别如下:
联系:
- (1)都是用于实现线程同步的机制,可以避免多个线程同时访问共享资源而导致的数据不一致或者数据竞争等问题。
- (2)都具有排他性,也就是在同一时刻只允许一个线程进行访问。
区别:
- (1)synchronized是Java语言内置的关键字,而ReentrantLock是Java的一个类,需要通过创建对象使用。
- (2)synchronized是非公平锁,它不能保证等待时间最长的线程优先获得锁;而ReentrantLock可选择公平锁和非公平锁,默认为非公平锁。
- (3)ReentrantLock相比synchronized提供了更强大的功能,如可重入、可响应中断、可超时等特性,因此在某些情境下ReentrantLock比synchronized更加灵活。
- (4)synchronized在代码块退出时自动释放锁,而ReentrantLock需要手动unlock()方法来释放锁。
- (5)ReentrantLock还支持Condition对象控制线程等待和唤醒操作,而synchronized则需要基于Object的wait、notify和notifyAll方法实现。
九、什么是乐观锁和悲观锁?
乐观锁和悲观锁是两种实现并发控制的方式。
- (1)悲观锁是一种“悲观”的思想,认为在数据访问时一定会发生冲突,因此每次在访问数据时都会先获取锁,避免其他线程对数据的修改。Java中的synchronized和ReentrantLock就是悲观锁的实现。
- (2)乐观锁是一种“乐观”的思想,认为在大多数情况下并发冲突是不会发生的,因此在访问数据时不会立即加锁,而是先进行尝试更新操作。如果发现冲突,就会进行重试,直到成功为止。Java中的CAS(Compare and Swap)、Atomic、StampedLock等机制就是乐观锁的实现。
悲观锁和乐观锁各有优缺点,选择哪种锁取决于具体的业务场景和性能要求。
悲观锁的优点在于:
- 实现简单,不需要考虑版本号等复杂的机制;
- 线程获取锁之后可以安全地进行修改操作。
悲观锁的缺点在于:
- 多线程并发访问时,可能会造成线程阻塞,降低系统的并发性能;
- 锁竞争激烈时,容易出现死锁等问题。
乐观锁的优点在于:
- 不需要加锁,可以提高系统的并发性能;
- 对于读操作比较频繁的情况,乐观锁的效率更高。
乐观锁的缺点在于:
- 实现相对复杂,需要考虑版本号等机制;
- 在高并发环境下,会出现ABA问题(即在执行CAS操作时,由于在中间的操作中,值曾经被其他线程修改过,导致CAS操作成功,但是实际上值已经发生了变化)。
十、什么是 CAS 操作?相对于锁有哪些优势?
CAS是Compare and Swap的缩写,中文意思是"比较并交换"。这是一种用于并发编程的原子操作,它通过比较内存值的方式来判断在执行操作之前是否有其他线程对内存进行了修改。如果没有,则执行操作;如果有,则不执行操作。CAS操作通常用于实现乐观锁,以确保多个线程对共享资源进行修改时的并发安全性。
CAS 操作与锁相比有以下优势:
- (1)非阻塞:在使用锁时,当一个线程持有锁时,其他线程需要等待该线程释放锁才能访问共享资源。而使用 CAS 操作时,如果某个线程的比较值和当前内存位置的值不同,它可以立即返回,而不需要等待其他线程的操作完成。
- (2)无死锁:由于 CAS 操作不需要加锁,所以它不会像锁一样出现死锁问题。
- (3)高性能:相对于锁,CAS操作的性能更高.因为锁对于并发控制是基于同步的机制,需要频繁地上下文切换和调度,而CAS操作是基于硬件的支持,可以更快地完成操作。
注:CAS 操作适用于只有一个共享变量的情况。如果需要保护多个共享变量的原子性操作,还需要使用其他的同步机制,如读写锁或者信号量等。
十一、什么是线程池?有什么用?该如何创建?
线程池是一种管理线程的机制,它可以预先创建一定数量的线程并将它们放入一个池子中,需要使用线程时,可以直接从池子中取出一个空闲线程来执行任务。当任务完成后,该线程并不会立即销毁,而是继续保留在池子中,等待下一次任务的到来。
线程池的主要作用可以体现在如下方面:
- (1)降低线程创建和销毁的开销。每次需要处理任务时,不必为之创建新的线程,从而减少了创建和销毁线程的开销。
- (2)提高响应速度。当有任务需要处理时,线程池中的线程可以立即执行,而不必等待新线程的创建。
- (3)控制并发线程数。线程池可以限制并发线程数,从而避免过度使用系统资源。
- (4)提高稳定性。线程池可以避免线程的过度创建和销毁,从而降低了线程泄露和内存泄露的风险。
在Java中,可以使用java.util.concurrent包中的ThreadPoolExecutor类来创建线程池。创建线程池的步骤包括:
- (1)创建ThreadPoolExecutor对象,并指定核心线程数、最大线程数、任务队列和线程池中线程的空闲时间等参数。
- (2)使用execute()方法将任务提交给线程池。
- (3)当不再需要线程池时,使用shutdown()方法关闭线程池。
十二、Java中如何解决线程安全问题?
线程安全问题是指多个线程同时访问一个共享的资源时,可能会产生冲突而导致结果不确定或不正确。在 Java 中,有一些常见的方法可以解决线程安全问题。
- (1)同步方法:使用 synchronized 关键字修饰方法,将对象锁定,确保同一时间只有一个线程能够访问该方法。
- (2)同步块:使用 synchronized 关键字锁定一个代码块,将需要同步的代码放入该块中,确保同一时间只有一个线程能够访问该块。
- (3)volatile 关键字:使用 volatile 关键字修饰变量,确保每次修改都立即写入主内存,从而保证多个线程之间的可见性。
- (4)Atomic 类:使用 java.util.concurrent.atomic 包下的原子类来操作变量,确保每个操作都是原子性的,从而保证多个线程之间的安全性。
- (5)Lock 接口:使用 java.util.concurrent.locks.Lock 接口来提供更加灵活的线程同步机制,它能够替代 synchronized 关键字来实现同步。
十三、什么是 volatile 关键字?有什么作用?
Volatile关键字是Java中用来修饰变量的一种关键字,其作用是保证多线程之间的可见性和有序性。它的主要作用是防止编译器对代码进行优化,从而避免出现意想不到的问题。
声明为volatile的变量会强制将修改的值立即写入主内存,并且读取时也会从主内存中获取最新值,而不是从CPU缓存中获取,从而保证了多线程之间的可见性。
另外,Volatile关键字还能够保证有序性,即禁止指令重排序优化。由于在多线程环境下,指令无法保证顺序执行,因此可能会导致某些代码的执行顺序与期望的不一致,使用volatile关键字可以避免这种情况的发生。
十四、什么是AQS?
AQS指的是AbstractQueuedSynchronizer(抽象队列同步器)。它是Java并发包中进行同步操作的基础类。其核心思想是同步器的状态可以由0个或多个线程持有,线程之间通过内部状态的不同来决定是否需要阻塞等待或唤醒其他线程。
基于这种思想,AQS提供了一种实现同步器的通用框架,使用者可以继承AQS类并实现自己特定的同步器,如ReentrantLock、Semaphore、CountDownLatch等。AQS内部使用FIFO队列来维护线程的等待队列,以及利用volatile变量和CAS原子操作来实现线程之间的协调和同步。
AQS的作用是提供一种灵活的、可扩展的同步机制,允许多个线程之间共享同一个资源。通过AQS,我们可以实现自定义的同步器,满足不同的并发场景,从而提高程序的性能。
十五、如何实现线程间的通信?
线程间的通信是指不同线程之间进行信息交换,以实现协调和同步。在Java中,可以使用以下几种方式实现线程间的通信:
- (1)共享内存:多个线程可以访问同一块内存区域,从而实现数据共享。但是由于多线程同时访问共享内存区域可能会导致数据竞争和并发问题,需要使用同步机制来解决。
- (2)消息传递:多个线程可以通过消息传递的方式进行通信,每个线程都有自己的消息队列,可以向其他线程发送消息或接收消息。这种方式可以有效避免数据竞争和并发问题,但是需要使用复杂的消息传递机制。
- (3)管道:管道是一种半双工的通信方式,可以在两个线程之间传递数据。一个线程可以将数据写入管道,另一个线程可以从管道中读取数据。管道通常被用于父子进程之间的通信。
- (4)信号量:信号量是一种同步机制,可以用于控制多个线程对共享资源的访问。当一个线程需要访问共享资源时,它需要获取信号量的锁,其他线程必须等待该线程释放锁后才能访问共享资源。
- (5)互斥锁:互斥锁是一种同步机制,可以用于保护共享资源不被多个线程同时访问。当一个线程需要访问共享资源时,它需要获取互斥锁的锁,其他线程必须等待该线程释放锁后才能访问共享资源。