为什么学习多线程和并发编程
- 多线程和并发编程在Java中占据着举足轻重的地位。在面试中,多线程几乎是必问的问题,因此掌握基础知识至关重要。在实际工作中,虽然直接编写多线程代码的机会并不多,但在高并发环境下理解并发的原理和问题是必要的。例如,当大量请求同时访问同一接口时,如果不了解并发可能会导致的问题,就可能遇到性能瓶颈甚至系统崩溃。
基础知识:进程与线程
- 进程是资源分配的基本单位,是程序执行的一个实例。例如,一个运行中的应用程序就是一个进程。线程则是程序执行的最小单位,是CPU调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源。理解进程和线程的区别,对于深入理解Java多线程至关重要【82†source】。
多线程的实现方式
-
Java提供了三种实现多线程的方式:
1.继承Thread
类并重写run()
方法。这是最基础的多线程实现方式。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行逻辑
}
}
2.实现Runnable
接口。这种方式更灵活,允许线程类继承其他类。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行逻辑
}
}
new Thread(new MyRunnable()).start();
3.实现Callable
接口。这种方式可以有返回值,通常与FutureTask
配合使用。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
// 线程执行逻辑,返回值
return 123;
}
}
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
线程的状态
线程有五种状态:新建、就绪、运行、阻塞和死亡。新建状态是指创建了线程对象但还未调用start()
方法。调用start()
方法后,线程进入就绪状态,等待CPU调度。运行状态是线程正在执行run()
方法。阻塞状态指线程因某些原因放弃CPU使用权,暂时停止运行。最后,线程运行结束或因异常退出则进入死亡状态
Thread类的常用方法
Thread
类提供了多线程编程的基础。它的常用方法包括:
start()
: 启动一个新线程。run()
: 定义线程执行的操作。sleep()
: 让线程休眠一定时间。join()
: 等待线程执行完毕。interrupt()
: 中断线程。isAlive()
: 检查线程是否存活。setDaemon()
: 设置线程为守护线程。setName()
和getName()
: 设置和获取线程的名字。setPriority()
和getPriority()
: 设置和获取线程的优先级。
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
t.join();
Java的同步机制
在Java中,同步是保证多线程安全的关键。其中volatile
是一种轻量级的同步机制。它主要用于保证变量的可见性,即当一个线程修改了一个变量的值,其他线程能够立即看到这个修改。但是,volatile
不保证操作的原子性。与synchronized
(重量级锁)相比,volatile
不会引起线程上下文的切换和调度,从而减少了开销。但是,它的同步性较差,且使用时容易出错。
1.并发编程的核心概念
- 并发编程的三个基本概念包括原子性、可见性和互斥性:
- 原子性:指操作不可分割,完整性地执行,不会受到其他因素的干扰。
- 可见性:指一个线程修改的变量的值,其他线程能够立即知晓。
- 互斥性:通过锁来实现,确保同一时刻只有一个线程能访问特定资源。
2.Java内存模型JMM
- Java内存模型(Java Memory Model, JMM)规定了如何管理线程间的内存共享。JMM定义了线程和主内存之间的关系,线程的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存。这个机制确定了一个线程对共享变量的写入何时对其他线程可见。
3.volatile变量的特性
volatile
变量具有以下特性:- 保证可见性,但不保证原子性。
- 禁止指令重排,确保程序执行的顺序与代码的顺序相同。
volatile
适用于一写多读的场景,但在某些场景下不适用,如不满足原子性的操作。
4.Java中的锁
- Java提供了多种锁机制来处理并发编程的问题,包括:
-
乐观锁和悲观锁:
- 乐观锁:它假设最好的情况,即不会有并发冲突,因此不会立即锁定资源。通常通过版本控制实现,如使用版本号或时间戳。如果在更新资源时检测到版本不匹配,表示资源已被其他线程修改,更新操作会失败。
- 悲观锁:假设最坏的情况,即总是假定会发生冲突并立即锁定资源。传统的锁机制,如
synchronized
和ReentrantLock
,通常都是悲观锁的体现。
-
独占锁和共享锁:
- 独占锁:只允许一个线程持有锁,如
ReentrantLock
。其他线程必须等待该锁被释放。 - 共享锁:允许多个线程同时持有锁。例如,
ReadWriteLock
的读锁是共享的,可以被多个读线程同时持有。
- 独占锁:只允许一个线程持有锁,如
-
互斥锁和读写锁:
- 互斥锁:确保同一时间只有一个线程可以执行特定的代码段。
- 读写锁:允许多个读操作同时进行,但写操作会独占锁。
ReadWriteLock
提供了读锁和写锁。
-
公平锁和非公平锁:
- 公平锁:按照线程请求锁的顺序来分配锁,如
ReentrantLock
在创建时可指定公平性。 - 非公平锁:允许抢占,即新请求的线程可能会在排队的线程之前获得锁。
- 公平锁:按照线程请求锁的顺序来分配锁,如
-
可重入锁:
- 又名递归锁,允许线程多次获得同一锁。
ReentrantLock
和synchronized
都是可重入锁。
- 又名递归锁,允许线程多次获得同一锁。
-
自旋锁:
- 线程在获取锁时,会循环检查锁是否可用,而不是立即进入阻塞状态。它避免了线程状态的切换带来的开销。
-
分段锁:
- 将数据分成不同的段,每段有自己的锁。通过这种方式,可以减少资源竞争,提高并发度。例如,在
ConcurrentHashMap
中使用。
- 将数据分成不同的段,每段有自己的锁。通过这种方式,可以减少资源竞争,提高并发度。例如,在
-
锁升级:
- Java虚拟机中采用了锁升级的概念,包括无锁、偏向锁、轻量级锁和重量级锁,根据竞争情况逐渐升级,以优化锁的性能。
-
锁优化技术:
- 包括锁粗化(将多次加锁解锁操作合并为一次,减少锁操作)和锁消除(JVM优化去掉不必要的锁)等技术,用于提升多线程程序的性能。
-
- 每种锁都有其特定的使用场景和优势,了解这些锁的不同可以帮助开发者更好地解决多线程中的同步问题。
5.锁的优化技术
- 锁的优化技术是提高多线程程序性能的重要手段。包括锁粗化(将多次锁请求合并为一次,减少锁的请求次数)和锁消除(JVM优化去掉不必要的锁)等。通过这些技术,可以减少锁的开销,提高程序的执行效率。
-
结论
- 了解和掌握多线程和并发编程的原理及其在Java中的实现是开发高效、稳定Java应用程序的关键。通过深入学习线程的生命周期、同步机制以及Java内存模型,开发者可以更好地理解并发环境下的程序行为,