从计算机的发展史上我们可以了解到,计算机是从单核单线程
发展到如今多核多线程
。
多核多线程的出现是计算机发展史上的一个自然演进,涉及到硬件和软件两个方面的考量。以下是一些主要的原因:
需求提高: 随着计算机应用的不断扩展和复杂性的增加,对计算能力的需求也在不断提高。单核处理器在性能上有一定的瓶颈,难以满足日益增长的计算需求。
摩尔定律: Moore’s Law 预测了集成电路上的晶体管数量每两年翻一番,这意味着在同样的芯片面积上可以集成更多的晶体管。然而,随着晶体管数量增加,单核性能提升的幅度逐渐减缓。
功耗和散热问题: 随着处理器频率的提高,功耗和散热问题变得日益突出。提高频率会导致处理器产生更多的热量,而散热成为一个严重的挑战。多核处理器通过在同一芯片上集成多个核心,能够在相同功耗范围内提供更多的计算能力。
并行计算需求: 许多应用程序都具有天然的并行性,可以通过并行计算来提高性能。例如,图形处理、科学模拟、数据处理等领域的应用,都可以通过同时处理多个任务或数据来加速计算。
多任务操作系统: 随着计算机操作系统的发展,多任务和多线程的概念变得日益重要。多核多线程的处理器可以更好地支持并发执行多个任务和线程,提高整个系统的响应性。
资源利用率: 多核多线程可以更好地利用计算机系统的硬件资源,提高系统整体的效率。在某些情况下,即使单个任务无法充分利用所有核心,多核处理器也可以同时执行多个任务,提高整个系统的吞吐量。
综合考虑以上因素,多核多线程体现了一种在硬件层面上应对性能需求和资源利用率的发展趋势。
竞态条件(Race Condition): 在多线程环境中,多个线程同时访问和修改共享的数据时可能会导致竞态条件,从而产生不确定的结果。Java 并发提供同步机制,如锁(Locks)和同步块(synchronized blocks),来解决竞态条件问题。
死锁(Deadlock): 死锁是指两个或多个线程相互等待对方释放锁,从而导致程序无法继续执行。Java 提供了各种工具来预防和解决死锁,如锁的顺序性和 java.util.concurrent
包中的高级并发工具。
线程安全性: 多线程环境中,多个线程同时访问共享数据可能导致不安全的操作。Java 并发提供了同步机制和线程安全的数据结构,如 ConcurrentHashMap
,来确保线程安全性。
内存可见性: 当多个线程同时访问共享变量时,一个线程对变量的修改可能对其他线程是不可见的。Java 内存模型(Java Memory Model,JMM)定义了一些规则来确保多线程环境下的内存可见性,同时 volatile
关键字也可以用于解决部分内存可见性问题。
性能优化: 在一些情况下,多线程并发可能导致性能问题,如线程竞争、上下文切换等。Java 并发提供了线程池、并发集合等工具来优化多线程编程的性能。
复杂的协调和同步: 在一些应用中,多个线程需要协同工作以完成某些任务。Java 提供了 CountDownLatch
、CyclicBarrier
、Semaphore
等同步工具来协调线程的执行。
并发数据结构: Java 提供了一些并发安全的数据结构,如 ConcurrentHashMap
、ConcurrentLinkedQueue
,以及 java.util.concurrent
包下的其他集合类,用于在多线程环境中安全地操作数据。
以下是一个线程不安全的示例,多线程对同一个共享数据进行数据操作,那么结果并不是预期结果
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
UnsafeCounter unsafeCounter = new UnsafeCounter();
// 创建两个线程,同时增加计数器
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
unsafeCounter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
unsafeCounter.increment();
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终计数器的值
System.out.println("Final Count: " + unsafeCounter.getCount());
}
}
Final Count: 16513 // 最终结果总是小于20000
在 Java 内存模型中,主要涉及到的概念包括主内存、本地内存、工作内存、原子性、可见性、有序性等。下面我们将详细讲解 Java 内存模型的一些规则。
CPU缓存是为了提高计算机系统的性能而引入的,但它也带来了可见性的问题。当一个线程修改了共享变量时,这个修改可能首先发生在线程的本地缓存中而不是主内存中,其他线程可能无法立即看到这个修改。
解决可见性问题的方法之一是使用volatile
关键字,它确保了对该变量的写操作会立即被其他线程看到。
public class SharedResource {
private volatile int sharedVariable;
public void modifySharedVariable(int newValue) {
sharedVariable = newValue;
}
}
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。在多线程环境中,原子性是确保多个线程同时访问共享变量时不会产生竞态条件的重要特性。
操作系统使用分时机制来调度线程的执行。在一个线程执行时,其他线程处于阻塞状态。这确保了某些操作的原子性,因为在分时机制下,某个线程执行时不会被其他线程中断。
public class AtomicOperation {
private int sharedVariable;
public synchronized void atomicModifySharedVariable(int newValue) {
sharedVariable = newValue;
}
}
有序性是指程序执行的结果与代码的顺序相一致。在多线程环境中,由于编译器和处理器为了提高执行速度
可能会对指令进行重排序
,可能导致代码的执行顺序与预期不一致。
public class ReorderingExample {
private int x = 0;
private boolean flag = false;
public void writer() {
x = 42;
flag = true;
}
public void reader() {
if (flag) {
System.out.println(x);
}
}
}
在上面的例子中,如果发生了指令重排序,flag
可能在x
之前被设置为true
,导致reader
方法输出的值不是预期的结果。
后续文章详细分析:关键字synchronized、volatile
定义了在多线程环境中,操作之间的执行顺序和可见性规则。Happens-before 规则是为了帮助程序员理解和确保正确的多线程程序行为。
Thread.join()
调用中的 join 操作。finalize()
方法的开始。它描述了在多线程环境中,对于单线程程序员来说,程序的执行表现应当是按照其在代码中指定的顺序执行的,即 as-if-serial。“as-if-serial” 原则保证了在多线程环境中,程序的执行在其在单线程中的执行的结果是一致的。