一、时间片
时间片是指操作系统分配给每个线程的一段固定时间,用于执行任务。在这段时间内,线程可以独占 CPU 资源。一旦时间片用完,操作系统会暂停当前线程的执行,并选择另一个线程来执行。
主要特性
1、公平性
通过时间片机制,操作系统确保每个线程都有机会执行,避免某个线程长时间占用 CPU。
2.、调度策略
不同的操作系统和调度器可能有不同的时间片长度和调度策略,以适应不同类型的应用程序。
3.、响应性
时间片机制提高了系统的响应性,确保交互式应用程序能够及时响应用户输入。
示例
假设操作系统的时间片为 10 毫秒,那么每个线程最多可以连续执行 10 毫秒。如果一个线程在这 10 毫秒内没有完成任务,操作系统会将其挂起,并选择另一个线程来执行。
二、上下文切换
上下文切换是指操作系统在不同线程之间切换时,保存当前线程的状态(上下文),并恢复下一个线程的状态,以便继续执行的过程。上下文包括程序计数器、寄存器、内存映射表等信息。
主要特性
1、保存和恢复
操作系统需要保存当前线程的上下文,并恢复下一个线程的上下文。
2、开销
上下文切换会消耗 CPU 时间和其他资源,因此频繁的上下文切换会影响系统的整体性能。
3、调度决策
操作系统根据调度算法决定何时进行上下文切换,常见的调度算法包括优先级调度、时间片轮转等。
示例说明
假设有两个线程 A 和 B,当前 CPU 正在执行线程 A。当线程 A 的时间片用完时,操作系统会执行以下步骤:
- 保存线程 A 的上下文(程序计数器、寄存器等)。
- 选择下一个线程 B。
- 恢复线程 B 的上下文。
- 开始执行线程 B。
影响因素
1、时间片长度
时间片越短,系统的响应性越好,但上下文切换的频率也会增加,导致更多的开销。
2、调度算法
不同的调度算法会影响上下文切换的频率和时机,例如优先级调度可能会减少上下文切换的次数。
3、线程数量
系统中运行的线程越多,上下文切换的频率越高,开销也越大。
减少上下文切换优化方案
(1)、减少线程数量
过多的线程会导致频繁的上下文切换。合理控制线程数量可以减少这种开销。线程少了,上下文切换自然也就少了。
实现:
使用线程池技术。合理优化线程池参数,如核心线程数、最大线程数、任务队列大小等,可以减少上下文切换。(一般线程池核心线程数量设置为CPU核心数的1-2倍比较合适,最大数量设置为核心数量的2倍)。
(2)、优化调度策略
根据应用程序的特点选择合适的调度策略,例如对于实时系统可以使用优先级调度。
常用策略:
1、增删改业务数据时,使用批处理操作,将多个小任务合并成一个大任务,减少任务调度的频率;
说明:批处理往往比一个一个新增或编辑处理更优。
2、使用线程池技术也算一种优化策略;
说明:可以限制最大的线程数量。
3、优先级调度允许高优先级的任务优先执行,减少低优先级任务的干扰。
说明:优先级高的线程大概率会先执行,先保证首要任务。
4、线程亲和性是指将特定的线程绑定到特定的 CPU 核心上,减少跨 CPU 核心的上下文切换。
说明:利用CPU特性,使用同一个核心处理任务。实际开始中不常用
import com.sun.jna.Native;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT;
public class ThreadAffinityExample {
static {
System.setProperty("jna.nosys", "true");
}
public static void main(String[] args) {
int cpuCore = 0; // 绑定到第一个 CPU 核心
Kernel32 kernel32 = Kernel32.INSTANCE;
WinNT.HANDLE currentThread = kernel32.GetCurrentThread();
kernel32.SetThreadAffinityMask(currentThread, 1 << cpuCore); // 设置亲和性
for (int i = 0; i < 10; i++) {
System.out.println("Task ID: " + i + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(100); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5、异步编程模型可以避免线程阻塞,减少上下文切换。
说明:异步可以更好利用CPU资源,减少线程阻塞。
(3)、减少阻塞性的 I/O 操作
避免因等待 I/O 完成而导致频繁的上下文切换。
阻塞性操作(如 I/O 操作、网络请求)会导致线程阻塞,从而增加上下文切换的频率。使用非阻塞或异步操作可以减少这种开销。
使用异步 I/O实现:
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ExecutionException;
public class AsyncIOExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("example.txt"), StandardOpenOption.READ);
channel.read(ByteBuffer.allocate(1024), 0, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
System.out.println("Read " + result + " bytes");
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("Read failed: " + exc.getMessage());
}
});
// 等待异步操作完成
Thread.sleep(2000);
}
}
(4)、使用原子类或无锁操作
Java的 java.util.concurrent.atomic
包中提供了原子类。通过使用 CAS(Compare-and-Swap)算法来实现原子操作,这确实可以在某些情况下减少上下文切换的频率和开销。本篇最后会对CAS算法做详细介绍。
示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger(0);
// 创建多个线程,每个线程递增计数器
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.incrementAndGet();
}
}).start();
}
// 模拟操作,目标为等待所有线程完成
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter.get());
}
}
(5)、锁优化
合理使用锁机制,避免因争用锁而导致频繁的上下文切换。
使用 ReentrantLock
示例
import java.util.concurrent.locks.ReentrantLock;
public class LockOptimizationExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockOptimizationExample example = new LockOptimizationExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join(); // 阻塞主线程,直到thread1完成为止
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.count);
}
}
(6)、使用线程局部变量(ThreadLocal)
线程局部变量可以在每个线程中存储独立的副本,避免线程之间的竞争,减少上下文切换。
示例:
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocalValue.set(10);
System.out.println("Thread 1: " + threadLocalValue.get());
});
Thread thread2 = new Thread(() -> {
threadLocalValue.set(20);
System.out.println("Thread 2: " + threadLocalValue.get());
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
三、资源限制
资源限制,指计算机的资源(如CPU、内存、存储空间和网络带宽)性能和效率都是有局限的。
如服务器的网络带宽只有2M,某个资源的下载速度是1M每秒,系统启动十个线程下载资源,下载速度也不会变成10M每秒,所以在进行并发编程时,要考虑到这些资源的限制。
常见优化方案
1、代码优化
优化算法和数据结构,减少不必要的计算和存储需求,提高程序执行效率。
2、缓存机制
利用缓存技术减少对数据库或其他后端服务的访问次数,加快响应速度。
3、负载均衡
通过负载均衡技术将请求分散到多个服务器上处理,避免单点过载,提高系统可用性和伸缩性。(springCloud,dubbo,tomcat+nginx,k8s)
4、并行与并发处理
合理利用多核处理器的能力,实现任务的并行或并发处理,加速任务完成。
5、资源复用
比如数据库连接池、线程池,http请求连接池等技术,通过复用已经创建的资源来减少创建销毁资源的成本。
Socket连接复用,这种方式在HTTP协议中通常被称为“持久连接”(Persistent Connections)或“HTTP Keep-Alive”。后面附录会介绍常用的方法。
6、优先级管理
为不同的任务和服务分配不同的优先级,确保关键任务能够优先获得资源支持。
7、超时机制
对于长时间运行的任务设定超时时间,避免因个别任务长时间占用资源而影响系统整体性能。
8、定期维护
包括但不限于清理不再需要的日志文件、临时文件,优化数据库索引等,以保持系统的高效运行。
四、线程优先级
线程优先级是操作系统调度线程的一种机制,用于确定哪些线程应该优先执行。合理设置线程优先级可以帮助优化系统的性能和响应性。
1、线程优先级的特点
(1)、优先级范围
- 不同的操作系统和编程语言对线程优先级的范围有不同的定义。在 Java 中,线程优先级的范围是从 1 到 10,其中:
MIN_PRIORITY
:1NORM_PRIORITY
:5(默认优先级)MAX_PRIORITY
:10
(2)、优先级继承
- 新创建的线程默认继承创建它的父线程的优先级。
(3)、优先级影响调度
- 高优先级的线程比低优先级的线程更有可能被调度器选中执行。
- 如果有多个高优先级的线程,调度器会在这几个线程之间进行轮询调度。
(4)、优先级不保证实时性
- 线程优先级并不能保证实时性,只是增加了高优先级线程被调度的概率。
- 操作系统调度器的实现细节会影响优先级的实际效果。
(5)、优先级的限制
- 操作系统可能对线程优先级有额外的限制,例如某些优先级可能需要管理员权限才能设置。
2、Java 中的线程优先级实现
在 Java 中,可以通过 Thread
类的方法来设置和获取线程的优先级。
(1)、设置线程优先级
Thread myThread = new Thread(() -> {
// 线程任务
System.out.println("Running thread with priority: " + Thread.currentThread().getPriority());
});
// 设置线程优先级
myThread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
// 启动线程
myThread.start();
(2)、获取线程优先级
Thread myThread = new Thread(() -> {
// 线程任务
System.out.println("Running thread with priority: " + Thread.currentThread().getPriority());
});
// 启动线程
myThread.start();
// 获取线程优先级
System.out.println("Thread priority: " + myThread.getPriority());
(3)、实例不同优先级的线程
public class ThreadPriorityExample {
public static void main(String[] args) {
// 高优先级
Thread highPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("High priority thread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 默认优先级
Thread normalPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Normal priority thread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 低优先级
Thread lowPriorityThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low priority thread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 设置线程优先级
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
normalPriorityThread.setPriority(Thread.NORM_PRIORITY);
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
// 启动线程
highPriorityThread.start();
normalPriorityThread.start();
lowPriorityThread.start();
}
}
五、时间片分配算法
操作系统的时间片分配算法是调度器用来决定哪个进程或线程在给定的时间内运行的关键机制。不同的调度算法适用于不同的应用场景,每种算法都有其特点和优缺点。
常见的时间片分配算法及其特点:
1. 先来先服务(First-Come, First-Served, FCFS)
描述:按照进程到达的顺序进行调度。
优点:实现简单,公平。
缺点:响应时间长,容易导致短作业被长作业阻塞,不利于交互式系统。
2. 短作业优先(Shortest Job First, SJF)
描述:优先调度预计运行时间最短的进程。
优点:平均等待时间和周转时间较短。
缺点:需要准确预测每个进程的运行时间,实现复杂,且容易导致长作业被饿死。
3. 时间片轮转(Round Robin, RR)
描述:每个进程轮流分配一个固定的时间片(时间量子),时间片结束后切换到下一个进程。
优点:实现简单,公平,适合交互式系统。
缺点:时间片大小的选择影响性能,时间片太小会导致过多的上下文切换,时间片太大则失去公平性。
4. 优先级调度(Priority Scheduling)
描述:根据进程的优先级进行调度,优先级高的进程优先执行。
优点:灵活,可以根据不同的需求调整优先级。
缺点:容易导致低优先级进程被饿死,需要引入老化机制来解决。
5. 多级反馈队列调度(Multilevel Feedback Queue, MLFQ)
描述*:结合了优先级调度和时间片轮转,使用多个优先级队列,进程根据其行为动态调整优先级。
优点:灵活性高,可以很好地适应不同类型的进程,适合复杂的系统。
缺点:实现复杂,需要精细调整队列和时间片的配置。
6. 最高响应比优先(Highest Response Ratio Next, HRRN)
描述:选择响应比最高的进程进行调度,响应比 = (等待时间 + 服务时间) / 服务时间。
优点:兼顾短作业和长作业,公平性好。
缺点:计算响应比需要额外的开销,实现相对复杂。
7. 实时调度算法
- 描述:专门用于实时系统的调度算法,确保关键任务按时完成。
- 常见算法:
1、最早截止时间优先(Earliest Deadline First, EDF):优先调度最早截止时间的任务。
2、固定优先级调度(Rate Monotonic Scheduling, RMS):优先级与任务周期成反比,周期短的任务优先级高。
优点:确保关键任务按时完成,适用于实时系统。
缺点:实现复杂,需要精确的时间和优先级管理。
8. 睡眠队列调度
- 描述:将等待某种资源的进程放入睡眠队列,资源可用时唤醒相应的进程。
- 优点:减少不必要的上下文切换,提高系统效率。
- 缺点:需要有效的资源管理和唤醒机制。
总结
1、每种时间片分配算法都有其适用的场景和优缺点。选择合适的调度算法需要根据系统的具体需求和特性来决定。例如,对于交互式系统,时间片轮转算法通常是一个好的选择;而对于实时系统,实时调度算法更为合适。多级反馈队列调度则适用于需要高度灵活性和适应性的复杂系统。
2、操作系统通过监控系统负载、进程特性、用户需求等因素,动态选择和调整调度算法,以优化系统的性能和响应性。具体的方法包括负载感知调度、优先级调整和队列管理等。通过这些方法,操作系统可以灵活地适应不同的应用场景,确保系统的高效运行。
六、附
(1)、CAS 算法
1、 CAS 算法简介
CAS(Compare-and-Swap)是一种无锁算法,用于在多线程环境中实现原子操作。CAS 操作包含三个参数:内存位置 V、预期值 A 和新值 B。
2、CAS 算法原理
1、如果内存位置 V 的值等于预期值 A,则将 V 的值设置为新值 B,并返回 true。
2、否则,不做任何操作,并返回 false。
3、CAS 算法的优势
1、无锁:CAS 算法不需要传统的锁机制,避免了线程阻塞和上下文切换。传统锁机制在多线程竞争激烈的情况下,会导致大量的上下文切换,而 CAS 算法通过乐观锁的方式减少了这种开销。
2、高性能:在大多数情况下,CAS 操作比传统的锁机制更快,因为它避免了锁的开销。
3、细粒度控制:CAS 算法可以提供更细粒度的控制,适用于需要高性能和低延迟的场景。
4、CAS 算法的局限性
1、ABA 问题:如果一个值从 A 变为 B,再变回 A,CAS 操作可能会误判为值没有变化。为了解决这个问题,可以使用带有版本号的 CAS 操作,或者使用 AtomicStampedReference
。
2、循环时间开销:在高竞争情况下,CAS 操作可能需要多次尝试才能成功,这会导致额外的 CPU 开销。
3、内存屏障:CAS 操作需要内存屏障来保证可见性和有序性,这可能会带来一定的开销。
5、CAS 对上下文切换的影响
1、减少线程阻塞:CAS 操作不需要传统的锁机制,因此不会导致线程阻塞。这意味着在高并发情况下,线程可以继续执行其他任务,而不是等待锁释放,从而减少上下文切换的频率。
2、提高系统响应性:由于线程不会被阻塞,系统的响应性会更高,特别是在处理大量并发请求时。
3、优化资源利用:CAS 操作可以更高效地利用 CPU 资源,减少因锁竞争导致的上下文切换开销。
6、伪代码示例
AtomicInteger伪代码
public final int incrementAndGet() {
for (;;) { // 死循环
int current = get(); // 获取当前的值
int next = current + 1; // 更新的值
if (compareAndSet(current, next)) { // 比较更新处理
return next;
}
}
}
解释:
if (compareAndSet(current, next))
使用 CAS 操作尝试将当前值从current
更新为next
。- 如果当前值仍然是
current
,则更新成功,返回新的值next
。 - 如果当前值已经被其他线程修改,则更新失败,继续循环重新尝试。
compareAndSet(CAS)操作的整体原子性确保了在检查和更新操作之间不会被其他线程干扰。
7、原子类如何实现线程安全的?
1、原子操作
- CAS 操作是原子性的,这意味着在执行
compareAndSet
方法时,其他线程不能在同一时刻修改相同的内存位置。(原子性是CPU硬件实现的) - 如果其他线程在
compareAndSet
操作期间修改了内存中的值,当前线程的 CAS 操作会失败,并返回false
。
2.、重试机制
- 如果 CAS 操作失败,通常会在更高层次的逻辑中实现重试机制。例如,在
incrementAndGet
方法中,如果 CAS 操作失败,会继续重试,直到成功为止。
8、CAS 的原子性
1、原子性
- CAS 操作是原子性的,这意味着整个操作(比较和交换)是不可分割的,要么全部完成,要么完全不发生。
- 在多线程环境中,即使有多个线程同时尝试执行 CAS 操作,硬件会确保只有一个线程的操作成功,其他线程的操作会失败并重试。
2、硬件支持
- 现代处理器提供了专门的指令来支持 CAS 操作,例如 x86 架构中的
CMPXCHG
指令。 - 这些指令在硬件层面确保了 CAS 操作的原子性。
(2)、连接复用技术
1、 使用HTTP Keep-Alive
HTTP 1.1 默认开启了Keep-Alive功能,这意味着在一个TCP连接上可以发送多个HTTP请求和响应,而不需要每次请求都重新建立连接。
示例:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpClientExample {
public static void main(String[] args) throws Exception {
String url = "http://example.com/api/data";
// 创建URL对象
URL obj = new URL(url);
// 打开连接
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
// 设置请求方法
con.setRequestMethod("GET");
// 开启Keep-Alive
con.setRequestProperty("Connection", "keep-alive"); // **重点,连接保活性**
// 发送请求
int responseCode = con.getResponseCode();
System.out.println("Response Code : " + responseCode);
// 读取响应
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 输出响应结果
System.out.println("Response: " + response.toString());
// 关闭连接
con.disconnect();
}
}
2、使用HttpClient库
Apache HttpClient 是一个更高级的HTTP客户端库,它提供了更丰富的功能和更好的性能。HttpClient 内置了对连接池的支持,可以自动管理连接复用。
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
public class HttpClientExample {
public static void main(String[] args) throws Exception {
// 创建连接池管理器
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
connManager.setMaxTotal(100); // 设置最大连接数
connManager.setDefaultMaxPerRoute(20); // 设置每个路由的最大连接数
// 创建HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connManager)
.build();
// 创建HttpGet请求
HttpGet httpGet = new HttpGet("http://example.com/api/data");
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
// 获取响应状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("Response Code : " + statusCode);
// 获取响应内容
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("Response: " + responseBody);
}
}
}
3. 使用OkHttp库
OkHttp 是另一个流行的HTTP客户端库,它也支持连接复用。原理也是使用连接池。
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class OkHttpExample {
public static void main(String[] args) throws Exception {
// 创建OkHttpClient实例
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new okhttp3.ConnectionPool( // 连接池
5, // 连接池空闲最大5个活跃连接数
5, // 最大空闲存在时间,即5分钟后如果连接还是空闲的就清除这个连接
java.util.concurrent.TimeUnit.MINUTES))
.build();
// 创建Request对象
Request request = new Request.Builder()
.url("http://example.com/api/data")
.build();
// 执行请求
try (Response response = client.newCall(request).execute()) { // 使用okhttl执行请求
// 获取响应状态码
int statusCode = response.code();
System.out.println("Response Code : " + statusCode);
// 获取响应内容
String responseBody = response.body().string();
System.out.println("Response: " + responseBody);
}
}
}
(3)、SOCKET、TCP、HTTP之间的区别与联系
1、两个计算机进行网络通讯时使用TCP协议就够了(三次握手完成了连接。服务器监听,客户端请求,连接确认),双方就能收发数据了,但是在解析数据时,没有指定数据格式规范,就会无法解析对方传来的数据;
即:你可以给一个外国人打电话,电话能打通,但听的懂吗?
2、HTTP协议解决了浏览器和服务器(或服务器和服务器)之间通讯协议的数据格式解析;大家都通过http协议的格式解析和发送报文,就能明白了。
即:我们电话打通后,就规定好都说中文,这样就都明白对方说的什么意思了。
简述:
TPC/IP协议是传输层协议,主要解决数据在网络中如何传输,而HTTP是应用层协议,主要解决如何包装数据。
Socket套接字是通信的基石(确定ip和端口),是程序员能够进行 TCP/IP(UDP) 编程的具体实现接口。
区别:
HTTP是应用层的协议,更靠近用户端;TCP是传输层的协议;而socket是从传输层上抽象出来的一个抽象层,本质是接口。
HTTP是短连接(一次连接就释放,但可以使用http连接池实现复用,可以参考上面的HttpClient或okHttpClient),Socket(基于TCP协议的)是长连接。
HTTP连接服务端无法主动发消息,客户端发送的每次请求服务器都需要回送响应。Socket连接双方请求的发送先后限制。
学海无涯苦作舟!!!