并发和并行的区别为:意思不同、侧重不同、处理不同。
一、意思不同
1、并发:并发是指两个或多个事件在同一时间间隔发生,把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
2、并行:并行是指两个或者多个事件在同一时刻发生,把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
二、侧重不同
1、并发:并发侧重于在同一实体上。
2、并行:并行侧重于在不同实体上。
三、处理不同
1、并发:并发在一台处理器上“同时”处理多个任务。
2、并行:并行在多台处理器上同时处理多个任务
22、多线程中的上下文切换指的是什么?
多线程中的上下文切换是指在多线程环境中,当一个线程执行完毕或被暂停时,操作系统需要将当前线程的上下文保存起来,并切换到另一个线程的执行。 上下文切换涉及保存当前线程的寄存器状态、程序计数器值等信息,并将另一个线程的上下文恢复到处理器中,以便继续执行。
(1)上下文切换通常发生在以下两种情况:
① 时间片用尽,在分时操作系统中,CPU为每个线程分配一定的时间片,当线程的时间片耗尽时,操作系统会将CPU的控制权交给下一个线程。
② 线程阻塞或等待,当线程需要等待某个事件(如I/O操作完成)或因互斥锁等原因进入阻塞状态时,操作系统会选择另一个可运行的线程
来继续执行。
(2)上下文切换包含两个过程:
① 保存状态,操作系统会将当前线程的执行状态(如寄存器值、程序计数器等)保存到内存中,这个过程称为“切出”。
② 加载状态,随后,操作系统会从内存中加载另一个线程的执行状态,使其继续在CPU上运行,这个过程称为“切入”。
上下文切换是操作系统多任务管理的一个重要组成部分,它允许多个线程在单个CPU上交替运行,从而实现并发执行。然而,频繁的上下文切换会增加系统的开销,因为每次切换都需要保存和加载线程状态,这会消耗CPU时间和内存资源。因此,在设计多线程程序时,应当尽量减少不必要的上下文切换,以提高系统的整体效率。
23、Java 中用到的线程调度算法是什么?
Java 中用到的线程调度算法主要是时间片轮转和优先级抢占,具体实现依赖于各种 JVM 和操作系统的情况。
1、优先级抢占
在这种模式下,更高优先级的线程会优先执行。与时间片轮转不同,线程不需要轮流运行,而是在满足条件后以无限期等待的方式运行。当更高优先级的任务出现时,调度器会中断当前线程并执行较高优先级的任务,这种方式也称为"抢占式调度"。
在 Java 中,线程的优先级通常是由 Thread 类提供的 setPriority() 方法或者相应构造函数来设置,优先级范围为 1-10 (默认为 5)。在 JVM 中,越高的优先级任务具有更多的执行机会,但并不能保证所有任务都获得机会。实际上,在某些情况下低优先级任务可能会一直等待而无法执行,而这种情况称为"饥饿问题"。
2、时间片轮转
它通过将处理器时间分成固定周期,并将每个任务分配固定的时间片进行执行,来确保公平性和响应性。
在 Java 中,时间片轮转算法通常是由 JVM 调度器来执行的,其中线程的执行被分为几个连续的时间片,JVM 会根据一定的规则决定当前线程活动时长是否已超过最大时间片,如果该时间已超过,则强制暂停当前线程的执行,并将 CPU 时间片分配给下一个线程。因此,这种算法可以避免线程的永久阻塞并提高系统的容错性。
3、其他算法
除了时间片轮转和优先级抢占外,Java 中还可以使用许多其他类型的调度算法,例如多级反馈队列调度、最短作业优先等,其中多级反馈队列调度也是比较流行且常用的。在该算法中,不同的任务被组织成一个任务序列,并分配到多个不同的容量栏以内。当任务进入队列后,它将被放置在第一列,然后逐渐向前移动,如果该任务需要更多时间才能完成,则移向含有更大时间片的队列。
24、Java中线程调度器和时间分片指的是什么?
线程调度器是操作系统内核的一部分,它的主要职责是管理和调度多个线程对CPU资源的使用。在多线程环境下,由于CPU资源有限,需要有一个机制来决定哪个线程将获得CPU的使用权。线程调度器根据不同的算法和策略,如先来先服务(FIFO)、最短作业优先(SJF)、最高优先级优先以及轮转(Round Robin)等,决定线程的执行顺序。
时间分片是一种确保多个线程能够公平共享CPU时间的技术。在这种机制下,CPU的时间被划分成许多小片段,每个片段称为一个时间片。
然后,这些时间片按照某种策略分配给处于可运行状态(Runnable)的线程。时间分片的大小和分配策略可以基于线程的优先级或者等待时间等因素来确定。
线程调度器通过时间分片技术,允许多个线程看似同时地使用单个CPU,从而实现了任务的并发执行。
25、什么是原子操作?Java中有哪些原子类?
原子操作是指一个或一系列操作,这些操作在执行过程中不会被其他线程打断,要么全部执行成功,要么全部不执行。 这种特性使得原子操作在并发控制中非常重要,特别是在多线程环境下。原子操作可以是一个步骤,也可以是多个操作步骤,但其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
Java中,原子类位于java.util.concurrent.atomic包中,主要包括以下几种:
(1)基本数据类型的原子类
AtomicInteger:提供原子更新的整数值。
AtomicBoolean:提供原子更新的布尔值。
AtomicLong:提供原子更新的长整数值。
(2)引用类型的原子类
AtomicReference:提供对引用类型的原子更新。
AtomicStampedReference:提供带有版本号的引用类型的原子更新。
AtomicMarkableReference:提供可标记的引用类型的原子更新。
(3)数组类型的原子类
AtomicIntegerArray:提供对整数数组的原子更新。
AtomicLongArray:提供对长整数数组的原子更新。
AtomicReferenceArray:提供对对象数组的原子更新。
26、wait与notify的区别?
多个线程在争夺同一个资源时,为了让这些线程协同工作、提高CPU利用率,可以让线程之间进行沟通,具体可以通过wait()和notify()实
现。
1. wait():使当前线程处于等待状态,即阻塞,直到其它线程调用此对象的notify()方法;
2. notify():唤醒在此对象监视器上等待的单个线程,如果有多个线程同时在监视器上等待,则随机唤醒一个;
3. notifyAll():唤醒在此对象监视器上等待的所有线程;
使用时需要注意几点:
1. 三个方法都是Object()类中定义的native方法,而不是thread类提供的,这是因为Java提供的类是对象级的,而不是线程级的;
2. 这三个方法都必须在synchronized修饰的方法或代码块中使用,否则会抛出异常;
3. 使用wait()时,为了避免并发带来的问题,通常建议将wait()方法写在循环的内部。
27、为什么 wait()、notify()、notifyAll()必须在同步方法或者同步块中被调用?
同步方法或同步块确保了在任一时刻只有一个线程可以执行这些代码区域,这样可以避免多个线程同时访问共享资源时产生竞争。
wait()方法会使当前线程进入等待状态,并释放对象的锁,而notify()和notifyAll()方法用于唤醒在该对象上等待的线程。
这些方法的设计是为了在多线程环境中协调线程的执行顺序和资源共享,如果没有同步机制,无法保证正确的等待和唤醒行为。
如果在非同步的上下文中调用这些方法,可能会导致所谓的“丢失唤醒”问题,即一个线程可能在没有获得锁的情况下被唤醒,这是非常危险的,因为它可能导致线程在不安全的情况下操作共享数据。
由于wait(), notify()和notifyAll()都是对象的方法,它们需要在同步方法或同步块中被调用,以确保线程之间的互斥和同步。这是因为在调用这些方法之前,调用线程必须已经获得了该对象的锁。
28、Thread 类中的 yield 方法有什么作用?
Thread 类中的yield 方法的主要作用是让当前线程放弃 CPU 时间片,使得其他具有相同优先级的线程有机会被执行。 这个方法是一个静态方法,意味着可以直接通过 Thread.yield() 调用,而不需要创建 Thread 的实例。它的主要目的是将当前线程的 CPU 使用权让给同优先级或者更高优先级的就绪状态线程。
具体来说,当调用 yield() 方法时,当前线程会从运行状态转变为就绪状态,这使得其他具有相同优先级的线程有机会获取 CPU 执行权。然而,yield() 方法仅是一个提示,不能保证其他线程一定会获得执行机会。线程调度器可能会忽略这个提示,因此调用 yield() 的线程有可能在进入到阻塞状态后马上又被执行。
29、yield、sleep和 wait 有什么区别?
yield(), sleep(), 和 wait() 都是Java多线程编程中用于控制线程执行的方法,但它们在功能和使用场景上有显著的区别。
- 所属类和功能差异:
- 使用场景和效果差异:
- wait():适用于需要等待某个条件成立的场景,调用后会释放对象锁,其他线程可以访问该对象。被notify()或notifyAll()方法唤醒后,线程会进入就绪状态,等待CPU调度。
- sleep():用于让当前线程暂停执行一段时间,适用于需要休息一段时间再继续执行的场景。不会释放对象锁,适用于不需要等待特定条件的场合。
- yield():用于让出CPU给同等优先级的线程执行,但不保证立即让出CPU,当前线程可能会再次被调度执行。
- 释放锁的差异:
- wait():会释放当前线程占有的对象锁,其他线程可以访问该对象。
- sleep():不会释放对象锁,当前线程在休眠期间,其他线程无法访问该对象的同步块。
- yield():不会释放对象锁,只是让出CPU给同等优先级的线程执行。
代码示例:
public class ThreadExample {
public static void main(String[] args) {
final Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1 is running.");
try {
Thread.sleep(2000); // 让Thread2有机会运行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is sleeping.");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
try {
lock.wait(); // 等待通知
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 is running after being notified.");
}
});
thread1.start();
thread2.start();
}
}
30、Java 如何实现多线程之间的通讯和协作?
在Java中,实现多线程之间的通讯和协作可以使用Object类中的wait()、notify()和notifyAll()方法,或者使用java.util.concurrent 包中的Condition接口提供的方法。
以下是使用wait()、notify()方法实现线程间通讯的一个简单例子:
class MyTask {
private boolean finished = false;
public synchronized void doWork() {
while (!finished) {
try {
wait(); // 线程等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("任务完成");
}
public synchronized void finishTask() {
finished = true;
notifyAll(); // 唤醒等待的线程
}
}
public class ThreadCommunication {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread worker = new Thread(() -> task.doWork());
worker.start();
// 做一些工作...
// 完成任务
task.finishTask();
}
}
使用Condition
接口实现通讯和协作:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
class ConditionTask {
private Lock lock = new ReentrantLock();
private Condition finished = lock.newCondition();
private boolean finishedWork = false;
public void doWork() {
lock.lock();
try {
while (!finishedWork) {
finished.await(); // 线程等待
}
System.out.println("任务完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public void finishWork() {
lock.lock();
try {
finishedWork = true;
finished.signal(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
}
public class ConditionCommunication {
public static void main(String[] args) {
ConditionTask task = new ConditionTask();
Thread worker = new Thread(() -> task.doWork());
worker.start();
// 做一些工作...
// 完成任务
task.finishWork();
}
}
31、JVM 对 Java 的原生锁做了哪些优化?
(1)自旋锁
在线程进行阻塞的时候,先让线程自旋等待一段时间,可能这段时间其它线程已经解锁,这时就无需让线程再进行阻塞操作了。
自旋默认次数是10次。
(2)自适应自旋锁
自旋锁的升级,自旋的次数不再固定,由前一次自旋次数和锁的拥有者的状态决定。
(3)锁消除
在动态编译同步代码块的时候,JIT编译器借助逃逸分析技术来判断锁对象是否只被一个线程访问,而没有其他线程,这时就可以取消锁了。
(4)锁粗化
当JIT编译器发现一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环中,此时会将加锁同步的范围粗化到整个操作系列的外部。
锁粒度:不要锁住一些无关的代码。
锁粗化:可以一次性执行完的不要多次加锁执行。
32、守护线程是什么?它是如何实现的?
守护线程(Daemon Thread)是一种在Java中用于执行后台任务的线程。守护线程的主要用途是为其他线程提供服务,例如垃圾回收、系统监控等。它们不会阻止程序的终止,当所有非守护线程结束运行时,程序和所有守护线程都会随之终止。
守护线程的实现方法是在线程启动之前调用setDaemon(true)
方法。这个方法必须在调用start()
方法之前设置,否则会抛出IllegalThreadStateException
异常。设置守护线程后,该线程将继续执行其任务,但当所有非守护线程结束时,守护线程也会随之终止。
常见的守护线程包括垃圾回收线程等。这些线程在程序运行时在后台提供通用服务,当所有用户线程结束时,守护线程也会自动结束,程序随之退出。
示例代码演示如何设置守护线程:
MyDaemonThread myDaemonThread = new MyDaemonThread();
myDaemonThread.setDaemon(true);
myDaemonThread.start();
在这个示例中,MyDaemonThread
类继承自Thread
类,并在其run()
方法中执行后台任务。通过调用setDaemon(true)
将其设置为守护线程,该线程将在后台执行任务,当所有非守护线程结束时自动终止。
33、为什么代码会重排序?
在Java中,代码重排序是指编译器和处理器为了优化程序性能,可能会调整程序中语句的执行顺序。这种优化是在不改变程序的单线程语义的前提下进行的。
重排序可能会导致如下问题:
可见性问题:如果一个线程基于重排序后的执行顺序对共享变量的写操作对另一个线程不可见,可能导致内存可见性问题。
原子性问题:某些操作(如i++)在多线程环境下可能不是原子的,重排序可能会破坏这些操作的原子性。
解决这些问题的方法是使用volatile关键字,它可以防止重排序,确保变量的可见性;或者使用synchronized关键字,它不仅确保可见性,还确保了原子性,还可以通过final关键字来避免重排序导致的问题。
示例代码:
class Example {
volatile boolean flag;
public Example() {
flag = false; // 显式初始化,防止重排序
}
public void start() {
new Thread(() -> {
while (!flag) {
// 循环体
}
}).start();
}
public void stop() {
flag = true; // 更新变量,导致可见性问题
}
}
在这个例子中,flag
变量被声明为volatile
,这样在多线程环境下,就不会出现因为重排序导致的可见性问题。
34、java如何实现线程的同步
在Java中,实现线程同步的主要方式有以下几种:
1.同步代码块:使用synchronized
关键字来保护代码块,确保同一时刻只有一个线程可以进入该代码块。
public void synchronizedMethod() {
synchronized(this) {
// 需要同步的代码
}
}
2.同步方法:在方法声明上使用synchronized
关键字,这样的方法称为同步方法。
public synchronized void synchronizedMethod() {
// 需要同步的代码
}
3.同步锁:使用ReentrantLock
类来实现更灵活的同步控制。
import java.util.concurrent.locks.ReentrantLock;
public class MyClass {
private final ReentrantLock lock = new ReentrantLock();
public void myMethod() {
lock.lock();
try {
// 需要同步的代码
} finally {
lock.unlock();
}
}
}
4.使用volatile关键字修饰共享变量,可以确保线程之间的可见性,但不提供原子性。
public class SharedObject {
public volatile int sharedCount = 0;
public void increment() {
sharedCount++; // 这个操作是非原子的,可能需要同步
}
}
- 使用
Atomic*
类,这些类提供了原子操作的支持,例如AtomicInteger
。
import java.util.concurrent.atomic.AtomicInteger;
public class SharedObject {
public AtomicInteger sharedCount = new AtomicInteger(0);
public void increment() {
sharedCount.incrementAndGet(); // 这是一个原子操作,不需要同步
}
}
35、说说多线程的三大特性
多线程的三大特性是原子性、可见性和有序性。
原子性
原子性是指一个操作或者一系列操作要么全部执行,要么都不执行,不能被其他线程打断。例如,银行转账操作必须保证原子性,即从一个账户扣款和向另一个账户存款这两个操作要么同时成功,要么同时失败,不能出现部分成功的情况。
可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改后的值。由于JVM的优化和CPU的缓存机制,一个线程对共享变量的修改可能不会立即对其他线程可见,这就是所谓的“可见性问题”。
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。然而,为了优化性能,JVM和CPU会对指令进行重排,这可能会导致代码的执行顺序与代码中的顺序不同。为了确保有序性,Java提供了volatile关键字和synchronized关键字,它们可以保证指令的执行顺序与代码顺序一致
36、java 有序性问题?
在Java中,有序性问题通常指的是当多个线程并发访问共享数据时,如何保证线程之间的有序性,避免出现意外的行为,如竞态条件、数据不一致等问题。
Java提供了volatile关键字来保证可见性,但不提供任何原子性保证,它不能用于实现计数器、同步队列等。
另外,Java提供的synchronized关键字和Lock接口可以实现原子性,也就是同一时刻只有一个线程可以执行被锁保护的代码块,从而保证了有序性。
下面是一个使用synchronized关键字来保证线程之间有序性的例子:
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++; // 这个操作是线程安全的,因为它被synchronized修饰
}
public synchronized int getCount() {
return count; // 这个操作是线程安全的,因为它被synchronized修饰
}
}
在这个例子中,increment方法和getCount方法都被synchronized关键字修饰,这意味着在同一时刻只有一个线程可以进入这两个方法中的任何一个,从而保证了操作的有序性。
37、Java中提供了哪些类解决多线程特性问题?
Java中提供了多种类和方法来解决多线程特性问题,包括同步锁、原子操作、并发集合、线程局部存储、读写锁和信号量等。
首先,Java提供了同步锁机制,通过synchronized关键字或java.util.concurrent.locks包下的锁类(如ReentrantLock)来保护共享资源,确保一次只有一个线程能够访问受保护的代码块。
其次,Java利用原子变量类(如AtomicInteger、AtomicBoolean等),提供无锁的线程安全操作,避免线程安全问题。
此外,Java提供了并发集合类(如ConcurrentHashMap、CopyOnWriteArrayList等),这些集合类在内部实现了线程安全的数据结构,适用于并发编程。
另外,Java还提供了线程局部存储(ThreadLocal类),为每个线程提供独立的变量副本,避免线程间的变量冲突。
对于读写操作频繁的场景,可以使用读写锁(ReadWriteLock),允许多个线程同时读取,但写入时独占访问权。
最后,使用信号量(Semaphore类)可以控制对有限资源的访问,适用于需要控制访问权限的场景3。
38、说说你对内存区域的理解?
JVM在运行时,会将其管理的内存区域划分为方法区、堆、虚拟机栈、本地方法栈和程序计数器5个区域; 方法区和堆是所有下城共享的区域; 虚拟机栈、本地方法栈、程序计数器是各个线程私有的.
39、说说你对Java内存模型的理解?
Java内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)的一部分,它定义了共享变量的可见性、原子性和有序性在多线程环境下的行为。Java内存模型是围绕着在并发编程中可能出现的各种问题而建立的,它确保了在多线程环境下,对共享变量的访问是正确和高效的。
1. 主内存与工作内存
Java内存模型主要分为主内存(Main Memory)和工作内存(Working Memory)两部分:
- 主内存:是Java虚拟机中所有线程共享的内存区域,用于存储共享变量。
- 工作内存:是每个线程私有的内存区域,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。
2. 变量访问规则
线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
3. 原子性、可见性和有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的:
- 原子性:保证了指令的不可分割性,即操作一旦开始就不会被线程调度机制中断。
- 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
总的来说,Java内存模型是一个复杂但强大的工具,它帮助Java程序员在多线程环境中编写安全、高效的代码。通过理解和应用Java内存模型,程序员可以更好地掌握并发编程的核心概念,并编写出更健壮、更高效的并发程序。
40、说说你对Happens-Before原则的理解?
Happens-Before原则是Java内存模型中用于描述两个操作之间的可见性和有序性的一种规则。这个原则非常重要,因为它帮助开发者理解在多线程环境下,不同线程之间的操作是如何相互影响和可见的。
Happens-Before原则的基本内容
- 程序顺序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。这保证了程序内指令的基本顺序性。
- 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这意味着,当一个线程释放了一个锁,那么之后其他线程获取这个锁的操作一定是在释放锁的操作之后发生的。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。这保证了volatile变量的可见性,即当一个线程修改了一个volatile变量,其他线程能够立即看到这个修改。
- 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,那么可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。这意味着,当一个线程启动后,它的所有操作都是在start()方法调用之后发生的。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过interrupted()方法检测到是否有中断发生。
- 线程终结规则:线程中所有的操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
Happens-Before原则的意义
Happens-Before原则为Java程序员提供了一种判断操作之间是否存在先行发生关系的方法。如果两个操作之间存在先行发生关系,那么第一个操作的执行结果将对第二个操作可见,且第一个操作的执行顺序排在第二个操作之前。
这个原则对于编写正确的并发程序非常重要,因为它帮助程序员理解在多线程环境下,不同线程之间的操作是如何相互影响和可见的。通过遵循Happens-Before原则,程序员可以编写出更安全、更高效的并发程序。