目录
1. 线程
线程的启动、运行、中断、同步等。
1.1. 线程的实现方式
Java提供了三种实现线程的方式:继承Thread类、实现Runnable接口、实现Callable接口。
1. 继承Thread类:创建自定义线程类继承Thread类,重写run()方法,调用start()启动线程。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类的线程");
}
}
2. 实现Runnable接口:创建自定义线程类实现Runnable接口,实现run()方法,通过Thread对象启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口的线程");
}
}
3. 实现Callable接口:创建自定义线程类实现Callable接口,实现call()方法,通过FutureTask启动线程,call()方法有返回值。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "实现Callable接口的线程";
}
}
这三种方式的对比:
- 继承Thread类:简单,但Java不支持多继承,继承了Thread就无法继承其他类。
- 实现Runnable接口:适合多个线程共享一个资源,可以继承其他类。
- 实现Callable接口:可以有返回值,用Future接口获取返回值。
推荐使用实现Runnable接口或Callable接口的方式实现多线程,比较灵活,可以避开Java单继承的限制。
继承Thread类实现多线程是最简单的方式,但由于Java的单继承特性,使其不太灵活。实现Runnable接口或Callable接口实现多线程就比较灵活,并且可以继承其他类,所以更推荐后两种方式实现多线程。
1.2. 线程的启动
Java线程的启动主要有两种方式:
1. 继承Thread类,重写run()方法,调用start()方法启动线程。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程启动");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
2. 实现Runnable接口,重写run()方法,通过Thread对象启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程启动");
}
}
public static void main(String[] args) {
Runnable target = new MyRunnable();
Thread thread = new Thread(target);
thread.start(); // 启动线程
}
注意:
1. 不能直接调用run()方法启动线程,必须调用start()方法启动。
2. start()方法会执行线程的调度,最终调用该线程的run()方法。
3. 一个线程对象只能start()一次,第二次start()会抛出IllegalThreadStateException。
4. start()方法是一个native方法,它会开启一个新的线程执行run()方法。
5. 新线程启动后会从run()方法开始执行代码。
6. run()方法运行在新线程中,而start()方法运行在调用线程中。
7. 新线程和调用线程是并发执行的。
要清楚start()方法的作用是启动一条线程,真正的线程执行逻辑在run()方法中。 理解这一点,才能正确理解线程的启动过程。
1.3. 线程的中断
Java线程的中断主要涉及以下方法:
- thread.interrupt():中断线程。
- thread.isInterrupted():判断线程是否被中断,返回中断状态。
- thread.interrupted():判断线程是否被中断,并清除中断状态。
当一个线程调用interrupt()方法中断另一个线程时,如果目标线程处于阻塞、限期等待或者无限期等待状态,那么将会抛出InterruptedException,从而提前结束该线程。
如果目标线程没有处于上述状态,调用interrupt()方法只是设置目标线程的中断标志,并不会直接终止目标线程。此时目标线程必须通过定期检测中断标志以响应中断操作。
示例1:在线程运行过程中定期检测中断标志并响应
public void run() {
while (!isInterrupted()) {
// ...
}
System.out.println("线程中断");
}
示例2:在线程阻塞时进行中断响应
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程中断");
}
}
总结:
1. 调用thread.interrupt()方法中断线程,这时只是设置线程的中断标志。
2. 中断相应有两种方式:定时检测中断标志、在阻塞方法中捕获异常。
3. catch块中调用thread.interrupted()清除中断标志,否则isInterrupted()仍返回true。
4. 中断并不代表线程立即结束,需要线程自己进行中断响应处理。
5. 不处于阻塞状态的线程,中断效果依赖于线程本身的配合,如果线程忽略了中断信号,中断效果就不会出现。
1.4. 线程的等待与唤醒
Java线程的等待与唤醒主要涉及Object类的wait()、notify()、notifyAll()方法:
- wait():使当前线程等待,直到另一个线程调用notify()或notifyAll()方法或超时。
- notify():唤醒一个等待对象的线程。
- notifyAll():唤醒所有等待对象的线程。
这些方法必须在同步方法或同步代码块中调用。它们的作用是控制线程的等待与唤醒。
示例:生产者消费者问题
public class WaitNotifyDemo {
public static void main(String[] args) {
Message msg = new Message();
Producer producer = new Producer(msg);
Consumer consumer = new Consumer(msg);
producer.start();
consumer.start();
}
}
// 生产者线程
public class Producer extends Thread {
Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
msg.push(i);
System.out.println("生产了:" + i);
}
}
}
// 消费者线程
public class Consumer extends Thread {
Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
msg.pop();
System.out.println("消费了:" + i);
}
}
}
// 信息队列
public class Message {
private int message;
public synchronized void push(int message) {
if (this.message != 0) { // 如果消息队列不为空,等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.message = message;
this.notify();
}
public synchronized int pop() {
if (message == 0) { // 如果消息队列为空,等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int message = this.message;
this.message = 0;
this.notify();
return message;
}
}
这是使用wait()和notify()方法实现生产者消费者模式的经典示例,是使用多线程编程必不可少的知识,也是面试中常问的内容。生产者线程生产消息,消费者线程消费消息,消息队列用来缓存消息。
1.5. 线程的睡眠
Java线程的睡眠使用Thread.sleep(long millis)方法实现,它的作用是使当前线程睡眠指定毫秒数。
示例:
public void run() {
try {
Thread.sleep(1000); // 线程睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sleep()方法会抛出InterruptedException异常,所以通常放在try/catch中使用。
sleep()方法的一些注意点:
1. sleep()使当前线程进入阻塞状态,该线程不会释放锁。
2. 睡眠时间不准确,只能理解为至少睡眠指定毫秒数,实际睡眠时间可能更长些。
3. 睡眠时间到了后线程会自动苏醒,不需要唤醒。
4. 睡眠期间可以被中断,从而抛出InterruptedException。
5. 睡眠结束后,从睡眠点继续执行,不会跳到finally块。
6. 同步方法或同步代码块内调用sleep()方法,其他线程无法访问同步资源,效果类似wait()方法。
7. sleep()方法产生的阻塞效果仅作用于线程级别,不会像I/O操作那样作用于进程级别。
sleep()方法常见用途:
1. 线程休眠:用于线程定时执行或等待其他线程执行完再继续。
2. 避免竞争:使线程在某段时间内停止,以免竞争临界资源。
3. 定时任务:用于实现定时任务。
4. 轮询:用于线程轮询,间隔一定时间检测条件然后继续执行。
总结:
sleep()方法使线程睡眠一定毫秒数,可以用来实现定时执行、线程等待和协调等功能,是控制线程运行效果的简单工具,掌握它的用法有助于我们编写高质量的多线程程序。面试中也经常会问到sleep()方法相关的内容,所以需要理解透彻。
1.6. 线程的同步
Java线程的同步主要涉及synchronized关键字和Lock接口的使用。它们的作用是实现线程间的互斥和同步。
synchronized关键字:
- 同步方法:将synchronized修饰的方法称为同步方法,它的作用是使该方法在同一时刻最多只有一个线程执行。
- 同步代码块:将synchronized(锁对象)修饰的代码块称为同步代码块,它的作用是使锁对象对应的同步代码块在同一时刻最多只有一个线程执行。
示例:
public synchronized void method() { // 同步方法
// ...
}
public void method() {
synchronized(this) { // 同步代码块
// ...
}
}
Lock接口:
Lock接口提供了比synchronized更广泛的锁定操作,它包含了获取锁(lock())、释放锁(unlock())和尝试非阻塞获取锁(tryLock())等方法。
ReentrantLock是Lock接口的实现类。
示例:
Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// ...
} finally {
lock.unlock(); // 释放锁
}
两者对比:
- synchronized是Java内置关键字,Lock是Java API。
- synchronized无法判断获取锁的状态,Lock可以判断是否获取到锁。
- synchronized会自动释放锁,Lock必须手动释放锁。
- synchronized线程阻塞时,其他线程也会阻塞。Lock线程阻塞时,其他线程可以执行。
- Lock更加灵活,可以设置超时方法和尝试获取锁。
总结:
synchronized和Lock都是实现线程同步的工具,但相比而言,Lock的功能更加强大一些。但是对于绝大多数的同步需求来说,synchronized已经足够。
理解Java线程同步的实现方式主要是synchronized关键字和Lock接口的使用。要掌握它们的特点,区别和应用场景,这是线程编程的基础内容之一,也是面试高频考察点。
1.7. 线程的状态
Java线程的状态主要有以下几种:
- NEW: 新创建的线程,还未启动。
- RUNNABLE: 运行状态,Java线程将操作系统中的可运行线程映射为RUNNABLE状态。
- BLOCKED: 阻塞状态,表示线程正在等待监视器锁定。
- WAITING: 等待状态,表示线程正在等待另一个线程执行某个操作。
- TIMED_WAITING: 超时等待状态,表示线程正在等待另一个线程执行操作,并且有超时时间。
- TERMINATED: 终止状态,表示线程已经执行完毕。
这些状态之间的转变关系如下:
NEW -> RUNNABLE
RUNNABLE -> BLOCKED
BLOCKED -> RUNNABLE
RUNNABLE -> WAITING
WAITING -> TIMED_WAITING
TIMED_WAITING -> WAITING
WAITING -> RUNNABLE
RUNNABLE -> TERMINATED
举个例子:
- 当我们创建一个新线程时,它的状态就是NEW。
- 启动线程后,它的状态变为RUNNABLE,并准备被调度执行。
- 如果线程进行sleep操作,它的状态从RUNNABLE变为TIMED_WAITING。
- sleep时间结束后,状态从TIMED_WAITING变回RUNNABLE,线程重新开始执行。
- 线程执行完毕后,状态变为TERMINATED。
通过监控线程的状态变化,我们可以了解线程的执行流程,如果出现BLOCKED状态持续时间过长,可能表示线程出现了死锁。这些都是我们分析和调试线程时需要重点关注的点。
1.8. 线程的优先级
Java线程的优先级表示线程获取CPU执行时间的可能性。线程优先级用数字1-10表示,越大优先级越高,默认为5,但优先级并不保证执行顺序。优先级高的线程获取CPU执行时间的几率更大。
Java线程优先级范围是1-10,默认优先级是5。
- 优先级1-4表示低级优先级
- 优先级5-7表示中级优先级
- 优先级8-10表示高级优先级
但是优先级并不是绝对的,主要有以下几点原因:
1. CPU调度算法:CPU使用自己的调度算法决定哪个线程执行,优先级只是影响因素之一。
2. 线程抢占:高优先级线程可以抢占低优先级线程的CPU执行时间,但是只能在某些点抢占(如方法调用,循环等)。如果低优先级线程正在执行计算密集型任务,高优先级线程无法抢占。
3. 优先级反转:如果高优先级线程需要低优先级线程的资源,那么高优先级线程会被阻塞,导致优先级反转。
4. JVM实现选择:HotSpot VM会忽略Java线程优先级,自己实现调度。所以Java线程优先级实际效果依赖于JVM实现。
Java线程优先级不能作为程序正确性的保证,只能作为一个影响因素。程序的正确性更加依赖于正确的同步机制和锁。
总结:
1. Java线程优先级并不是绝对的,只是影响线程调度的一个因素。
2. 高优先级表示会提高获取CPU执行时间的几率,但不是可以完全控制执行顺序。
3. 优先级需要配合同步和锁机制来保证程序正确性。
1.9. 守护线程
Java守护线程是一种特殊的线程,用thread.setDaemon(true)将线程设置为守护线程,它会在主线程结束时自动结束。它的主要特征是:
1. 守护线程是为其他线程服务的线程。
2. 当普通线程都结束时,守护线程也随之结束。
3. 守护线程最典型的应用是为普通线程提供服务(如垃圾回收线程)。
相比而言,普通线程是可以独立运行的线程。当所有的普通线程结束时,程序结束。
可以通过调用 Thread.setDaemon(true) 方法将一个线程设置为守护线程。必须在线程启动前调用,否则会抛出IllegalThreadStateException。
示例:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.setDaemon(true);
thread.start();
// 主线程运行结束,守护线程也随之结束
// ...
}
}
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
// ...
}
}
}
上例中,我们将MyThread线程设置为守护线程,当主线程结束时,MyThread线程也会结束,尽管它是一个无限循环。
需要注意的是,守护线程不应该对普通线程产生影响。因为一旦普通线程结束,守护线程很快就会结束。
总结
1. 守护线程是为普通线程服务的线程。
2. 当所有普通线程结束时,守护线程也会结束。
3. 必须在线程启动前将其设置为守护线程。
4. 守护线程不应对普通线程产生影响。
2. 线程安全
线程安全是指一段代码在多线程环境下能正常执行,并且不会由于其他线程的并发执行而导致数据的错误。
线程安全有以下几个原因:
1. 共享数据:多个线程访问了同一份数据。
2. 状态变化:一个线程的执行依赖另一个线程的执行结果。
要实现线程安全,主要有以下几种方式:
1. 互斥同步(synchronized):使用synchronized关键字锁定代码块或方法,互斥锁确保同一时刻只有一个线程可以执行临界区代码。
synchronized (obj) {
// 临界区
}
2. 非阻塞同步(Lock):使用Lock锁定代码块,与synchronized相比具有更高的灵活性。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
3. 原子变量(Atomic):使用原子变量操作,确保变量的线程安全。
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
4. 互斥collections(HashTable,Vector等):使用线程安全的集合类,内部使用synchronized确保线程安全。
5. 线程本地存储(ThreadLocal):每个线程有自己的存储空间,可以存储线程私有的变量,避免共享数据冲突。
ThreadLocal<T> threadLocal = new ThreadLocal<>();
6. 不可变对象:不可变对象一旦初始化后无法修改,自然就是线程安全的。
final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
}
2.1. 共享数据
多个线程访问共享的可变数据,如果没有正确的同步可能导致数据竞争。所以需要使用同步机制来保证共享数据的线程安全。
数据竞争会导致以下问题:
1. 脏读:一个线程读取了其他线程更新了一半的数据。
2. 不可重复读:一个线程多次读取共享数据,却读取了其他线程更新的数据。
3. 幻读:一个线程读取了其他线程插入的数据,导致前后读取结果不一致。
那么,如何保证共享数据的线程安全呢?主要有以下几种方式:
1. synchronized:使用synchronized关键字锁定对象或者方法,只有得到锁的线程可以访问共享数据,实现互斥访问。
示例:
public synchronized void add(int num) {
count += num;
}
2. Lock:使用Lock接口实现锁,同样可以实现互斥访问共享数据。
示例:
Lock lock = new ReentrantLock();
lock.lock();
try {
count += num;
} finally {
lock.unlock();
}
3. volatile:使用volatile关键字可以保证共享数据的可见性,避免线程读取缓存数据的问题。但是volatile不能保证互斥访问,需要配合锁使用。
示例:
private volatile int count = 0;
public synchronized void add(int num) {
count += num;
}
4. 原子变量:使用原子变量作为基本类型,可以保证原子性访问,从而实现线程安全。
示例:
private AtomicInteger count = new AtomicInteger(0);
public void add(int num) {
count.addAndGet(num);
}
5. 不可变对象:使用不可变对象作为共享对象,可以避免线程安全问题。
不可变对象(Immutable Object)是对象中的数据在创建后不能被修改的对象。不可变对象可以简单的实现线程安全,因为对象的数据不会被改变。
不可变对象的优点:
1). 线程安全:不可变对象在并发环境下是固有线程安全的,因为对象的状态不能被改变。
2). 安全发布:不可变对象一旦构建,其状态就不能再被改变,所以可以安全的发布,无须担心线程安全问题。
3). 缓存:不可变对象可以安全的缓存和重用。
4).无需额外防护拷贝:通过返回对象的一个拷贝来保证线程安全。不可变对象本身线程安全,不需要这种手段。
创建不可变对象的关键点:
1). 对象创建后其状态不可以改变。
2). 将对象的所有字段设置为final,避免被修改。
3). 不提供任何可以修改对象状态的方法。
4). 确保对象本身和所有组成对象的不可变性。
5). 禁止这个类被继承。
示例:
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
这个例子中,Person类设置为final的,所有字段设置为final,没有提供可以修改状态的方法,所以这个类就是一个不可变类,可以在并发环境使用。
使用不可变对象可以简单实现线程安全。创建不可变对象需要遵循:对象状态不变、使用final字段、不提供修改状态方法、确保组成对象也不可变以及禁止继承等原则。
2.2. 可见性
可见性是指当一个线程修改了共享数据的值,其他线程能够立即看到修改的值。
在并发编程中,由于CPU缓存和线程上下文切换,一个线程修改的数据很可能不能立即对其他线程可见。这会导致线程安全问题。
Java使用volatile关键字来保证共享数据的可见性,从而避免以下问题:
1. 线程读取了过期的值:线程从自己的工作内存读取了过期的值,而没有读取主内存中的最新值。
2. 线程丢失对新值的更新:线程完成了对共享数据的更新,但是由于线程上下文切换,其他线程没有立即读取到新值。
使用volatile的原理:
1. 阻止指令重排序:编译器和处理器通常会对指令做重排序优化,volatile可以阻止这种重排序,保证程序的执行顺序按照代码的预期执行。
2. 强制从主内存读:线程访问到volatile变量时,会强制从主内存中读取,而不是从工作内存中读取。
3. 强制刷新到主内存:线程修改volatile变量时,会强制刷新到主内存。
4. 线程切换会刷新内存:当线程切换时,会将本地线程工作内存中的共享变量刷新到主内存中。
示例:
public class VolatileExample {
private volatile boolean running = true;
public void print() {
System.out.println("Entered print() method");
while (running) {
// ...
}
System.out.println("Exited print() method");
}
}
在这个例子中,running变量被设置为volatile,所以当running变量被修改为false时,print方法中的循环会立即结束,这是因为volatile保证了变量值的可见性。
使用volatile可以保证共享数据的可见性,避免线程读取stale value和丢失更新问题。volatile通过禁止重排序,强制从主内存读写,线程切换刷新主内存来实现可见性。
2.3. 有序性
在Java内存模型中,为了优化程序性能,编译器和处理器常常会对指令进行重排序。这个重排序如果不正确会导致线程安全问题。
有序性是指程序的执行顺序按照代码的预期顺序执行。保证有序性可以避免由重排序导致的线程安全问题。
程序的执行顺序按照代码的预期去执行。可以使用volatile和synchronized来保证有序性。
1. volatile:volatile关键字可以保证不同线程对一个volatile变量的操作顺序一致。
示例:
volatile boolean ready;
void prepare() {
ready = true; // 1
doSomething(); // 2
}
void finish() {
while (!ready) {} // 3
doSomethingElse(); // 4
}
在这个例子中,finish方法可能先执行doSomethingElse(),这是因为编译器重排序了3和4语句。但是不会重排序1和2语句或者3和4语句,因为ready变量被设置为volatile。所以finish方法在执行doSomethingElse()之前,一定会读取到prepare方法设置的ready=true。
2. synchronized:synchronized可以保证同一锁的两个同步块/方法之间的有序性。
示例:
synchronized (lock) {
doSomething(); // 1
}
synchronized (lock) {
doAnotherthing(); // 3
}
synchronized (lock) {
doSomethingElse(); // 2
}
在这个例子中,不管编译器和处理器如何重排序,doSomethingElse()方法总会在doAnotherthing()方法之前执行。因为两者获取的是同一把锁。
3. happens-before:是Java内存模型中的一个重要概念,它定义了两个操作之间的执行顺序。happens-before规则可以确保在这两个操作之间的所有内存操作都是可见的,从而实现有序性。
Java内存模型指定的happens-before规则:
1). 程序次序规则:一个线程内,按照代码顺序,前面的操作happens-before后面的操作。
2). 锁定规则:一个unLock操作happens-before后续的lock操作。
3). volatile变量规则:对一个volatile域的写操作happens-before任意后续的读操作。
4). 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5). 线程启动规则:Thread对象的start()方法happens-before此线程的其它操作。
6). 线程加入规则:Thread对象的结束操作happens-before对此线程对象的其他操作。
7). 线程中断规则:对线程interrupt()方法的调用happens-before被中断线程的代码检测到中断事件。
8). 对象终结规则:一个对象的初始化完成(构造函数执行结束)happens-before此对象的finalize()方法的开始。
9). 传递的参数happens-before对传递参数的方法的操作。
这些规则可以让编译器在重排序时遵循,保证有序性,实现线程安全。例如:
public class HappensBeforeExample {
private boolean ready;
private void prepare() {
ready = true;
}
private void work() {
while (!ready) {} // busy wait
}
}
在这个例子中,根据程序次序规则,prepare()方法会在work()方法的busy wait循环之前执行,所以可以避免work()一直自旋等待。
happens-before规则提供了一套理论基础来限定Java内存模型中不同线程之间操作的"重排序边界"。
2.4. 重排序
重排序是编译器和处理器为了优化程序性能而对指令序列进行的调整。如果重排序不当,会导致线程安全问题。使用volatile和synchronized可以避免重排序导致的线程安全问题。
Java内存模型定义了几种重排序:
1. 内存重排序:编译器可以重排序主内存的读写操作,只要不影响单线程程序的语义。
2. 前向重排序:处理器可以在不影响单线程程序语义的前提下,将后续指令的读写操作提前,这叫做前向重排序。
3. 后向重排序:处理器可以在不影响单线程程序语义的前提下,将先前的读写操作推迟,这叫做后向重排序。
这些重排序如果发生在多线程环境中,很容易会导致线程安全问题。比如:
- 线程1:
int a = 1; //1. write a
int b = 2; //2. write b
- 线程2:
int c = b; //3. read b
int d = a; //4. read a
由于重排序,线程2有可能在执行3之前先执行4,这就导致读取到错误的值。
要禁止重排序,可以使用:
1. volatile:volatile变量的读写不会被重排序。
2. synchronized:同一锁的读写不会被重排序。
3. final:被final修饰的字段的读写不会被重排序。
4. 锁:不同的锁之间的读写可以被重排序。
示例:
// 无volatile时 可能重排序为:
int a = 1; //3
int b = 2; //1
x = a; //4
y = b; //2
// 使用volatile后:
volatile int a = 1;
int b = 2;
x = a;
y = b;
使用volatile可以禁止将读写b的操作重排序到读写a之前。
2.5. 锁定
锁定是指当一个线程获取了对象的锁后,其他线程无法访问该对象,只有该线程释放锁后,其他线程才能获得锁并访问对象。这个特性可以用于保证线程间的安全访问和协调。
Java提供了两种锁机制:
1. synchronized:synchronized关键字可以锁定对象或方法,只有获得锁的线程可以进入临界区,其他线程会阻塞等待。
示例:
public synchronized void method() {
// 临界区
}
2. Lock: java.util.concurrent.locks.Lock接口提供了更灵活的锁操作。需要显式获取和释放锁。
示例:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
这两种锁机制都可以实现以下效果:
1. 互斥:同一时刻只有一个线程可以访问共享资源。
2. 有序性:根据获取锁的顺序来决定线程访问共享资源的顺序。
3. 可见性:当锁被释放时,会将工作内存中的共享变量刷新到主内存,保证可见性。
4. 避免重排序:获取锁和释放锁会形成一个happens-before关系,避免重排序导致的线程安全问题。
锁还具有以下特性:
1. 可重入:同一个线程如果已获得对象锁,可以再次获取该锁。
2. 公平和非公平:公平锁的锁定顺序严格按照请求顺序,非公平锁会忽略等待时间最长的线程。
3. 互斥与非互斥:读写锁允许同时有多个读线程访问资源,但是在写线程访问时只允许一个线程访问。
当多个线程访问共享资源时,使用锁可以很好的保证线程安全。但是锁也会带来性能损耗,需要权衡使用。
2.6. 线程封闭
线程封闭是一种简单的实现线程安全的方式。其核心思想是:不将一个可变对象的状态暴露给多个线程,使其状态仅超过单个线程可见。这样就不需要额外的同步措施来控制线程对该对象状态的访问。
实现线程封闭的主要方式有:
1. 局部变量:将变量声明为方法内的局部变量,这样只有方法内的线程可以访问它。
示例:
public void method() {
int count = 0; // 局部变量
// ...
}
2. 线程本地存储:使用ThreadLocal将对象的状态与线程绑定,每个线程只能访问自己线程的对象状态。
示例:
private ThreadLocal<Integer> count = new ThreadLocal<>();
public void increment() {
count.set(count.get() + 1);
}
3. 不共享可变状态:不将可变对象的状态在线程间共享传递。
示例:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
// 没有提供修改name的方法
public String getName() {
return name;
}
}
在这个例子中,Person对象的name字段不提供修改方法,所以其状态不会在线程间共享,实现了线程安全。
线程封闭的优点:
简单,无锁定开销。避免了同步带来的性能损耗。
缺点:
1. 无法在多个线程间共享对象的可变状态。
2. 增加编程难度,需要在设计时考虑线程封闭。
3. 只能应用于特定场景,不具有通用性。
线程封闭是一种简单高效的实现线程安全的方式。当对象的可变状态不需要在线程间共享时,可以考虑使用线程封闭的方式来设计。
2.7. 线程隔离
可以通过互斥锁,读写锁,信号量,线程本地存储,线程池实现线程的隔离。
1. 互斥锁(Mutex):使用synchronized关键字锁定代码块或方法,只有获得锁的线程可以进入临界区,其他线程会阻塞等待。
synchronized (obj) {
// 临界区
}
2. 读写锁(ReadWriteLock):适用于读多写少的场景,可以同时允许多个读线程进入临界区,但是只允许一个写线程进入。使用ReentrantReadWriteLock实现。
Lock lock = new ReentrantReadWriteLock();
// 读锁
lock.readLock().lock();
// 写锁
lock.writeLock().lock();
3. 信号量(Semaphore):控制进入临界区的线程个数,使用Semaphore实现。
Semaphore semaphore = new Semaphore(3); // 只允许3个线程同时进入临界区
semaphore.acquire(); // 获取令牌
// 临界区
semaphore.release(); // 释放令牌
4. 线程本地存储(Thread Local Storage):每个线程有自己的存储空间,可以储存线程私有的变量。使用ThreadLocal实现。
ThreadLocal<T> threadLocal = new ThreadLocal<>();
threadLocal.set(t); // 设置线程本地变量
T t = threadLocal.get(); // 获取线程本地变量
5. 线程池(ThreadPool):通过线程池控制进入临界区的线程个数。使用Executors或ThreadPoolExecutor实现。
3. 锁
Java中的锁主要分为公平锁和非公平锁两种:
1. 公平锁:线程获取锁的顺序按照线程请求锁的时间顺序来分配锁。公平锁保证线程获取锁的顺序,避免锁的"饥饿"问题,但是并发性较低。
2. 非公平锁:线程获取锁的顺序不按照请求锁的时间顺序来分配锁。非公平锁的并发性较高,但是可能会出现锁的"饥饿"问题。
Java中的锁类型主要有:
1. 互斥锁:用于保护共享资源,同一时间只允许一个线程访问。主要有synchronized和ReentrantLock两种实现,默认都是非公平锁。
- synchronized是Java中的关键字,是一种非公平的互斥锁。
- ReentrantLock是java.util.concurrent包中的锁,可以指定是否使用公平锁,默认也是非公平锁。
2. 读写锁:允许同时有多个读线程访问资源,但是在写线程访问资源时,都需要互斥。主要有ReentrantReadWriteLock实现,也可以指定公平策略。
3. 信号量:用于控制访问资源的线程数量。Semaphore是其主要实现,也可以指定公平策略。
4. 栅栏:用于让一组线程等待直到某个阈值条件达成。主要有CyclicBarrier实现,也可以指定公平策略。
5. 锁接口:主要包括Lock、ReadWriteLock和Condition三个接口。其中ReentrantLock、ReentrantReadWriteLock是对Lock接口的实现;Condition是对Object的wait/notify进行封装。这三个接口也都可以指定是否使用公平锁。
总之,公平锁可以保证线程平等获取锁,但是可能会降低并发度;非公平锁的并发度较高,但是可能会出现饥饿现象。在使用锁时需要考虑资源的特点选择合适的锁类型和公平策略。
Java中的锁主要有两种:互斥锁和读写锁。
1. 互斥锁:也称互斥同步器,用于保证同一时间只有一个线程可以访问共享资源。主要有synchronized和ReentrantLock两种实现。
- synchronized是Java中的关键字,是一种互斥锁。
- ReentrantLock是java.util.concurrent包中的锁,需要显式加锁和释放锁。
特点是同一时间只允许一个线程访问资源,但是可以被同一个线程多次获取,这种锁称为可重入锁ReentrantLock。例如:
public synchronized void method1() {
method2();
}
public synchronized void method2() {
// ...
}
method1和method2都被synchronized修饰,表示获取同一把锁,但是 method1中可以调用method2,这就是可重入的体现。
2. 读写锁:允许同时有多个读线程访问资源,但是在写线程访问资源时,都需要互斥。读写锁有ReentrantReadWriteLock实现。
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
rwl.readLock().lock();
// 写锁
rwl.writeLock().lock();
特点是在没有写操作时允许多个读操作并发访问,写操作时需要互斥。读写锁适用于读多写少的场景,可以大大提高资源的并发访问效率。
总之,互斥锁用于保护资源的互斥访问,读写锁在读多写少的场景下可以提高并发效率。锁可以指定是否公平,公平锁可以避免锁的饥饿问题,非公平锁有更高的并发度。在使用锁机制时,需要根据资源的特点选择合适的锁类型。
4. 死锁
Java线程死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,这组线程将无法推进下去。
产生死锁的四个必要条件:
1. 互斥条件:该资源任意一个时间点只由一个线程占用。
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺。
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
示例:
public class DeadLockDemo {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {}
synchronized (lock2) {
// do something
}
}
}
public void method2() {
synchronized (lock2) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {}
synchronized (lock1) {
// do something
}
}
}
}
线程T1执行method1,线程T2执行method2,在一定条件下会出现 T1获取lock1,T2获取lock2,然后T1试图获取lock2而阻塞,T2试图获取lock1而阻塞,两个线程相互等待而死锁。
避免死锁的方法:
1. 打破循环等待条件:避免线程间形成循环依赖关系。
2. 打破互斥条件:使用不互斥的锁实现,如读写锁。
3. 打破请求与保持条件:线程获取多个资源时按顺序获取。
4. 设置 timeout 超时时间:超时后自动释放锁。
5. 破坏不剥夺条件:允许抢占锁。
5. 同步机制
synchronized关键字,锁的使用,wait/notify等。
5.1. synchronized 关键字
它可以修饰方法和代码块,实现线程间的互斥访问。被synchronized修饰的方法或代码块在同一时刻只能被一个线程访问。- 修饰方法:该方法在运行时只能由一个线程访问。
- 修饰代码块:指定对象的该代码块在运行时只能由一个线程访问。
public class SyncDemo {
public synchronized void method() {
// ...
}
}
public class SyncDemo {
public void method() {
synchronized (this) {
// ...
}
}
}
5.2. volatile 关键字
它可以修饰变量,保证线程对该变量的可见性,即当一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile并不能保证原子性。
public class VolatileDemo {
volatile boolean flag = true;
public void write() {
flag = false;
}
public void read() {
if (flag) {
// ...
}
}
}
5.3. Lock 锁
Java提供的Lock接口用以实现同步访问。其主要方法为:
- lock():加锁。
- unlock():释放锁。
常用的实现类有:ReentrantLock、ReentrantReadWriteLock。
public class LockDemo {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
}
}
5.4. Condition 条件
Condition是JDK1.5后加进来的,它用来替代Object的wait()、notify()实现线程间的协调,可以更加精确地控制线程的等待与唤醒。
public class ConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
5.5. Atomic 类
这是java.util.concurrent.atomic包下的类,提供了线程安全的原子操作,比如增减原子操作:
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
- AtomicBoolean:原子更新布尔型这些原子类通过CAS(Compare-And-Swap)算法实现线程安全,性能较锁更高。
public class AtomicDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
Java提供了锁机制(synchronized、ReentrantLock)和非锁机制(volatile、Atomic类)两种同步方案。前者适用于同步时间较长的线程,后者适用于同步时间较短的线程,具有更高的性能。
6. 并发容器
BlockingQueue、ConcurrentHashMap等。
1. CopyOnWriteArrayList:写时复制的ArrayList,适用于读多写少的场景。示例:
List<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
// 读操作
for (String element : list) {
doSomething(element);
}
// 写操作,会产生复制,但不会影响正在进行的读操作
list.add("3");
2. ConcurrentHashMap:适用于高并发的HashMap,采用锁分段技术,每段只锁定自己区域的数据。示例:
Map<String, String> map = new ConcurrentHashMap<>();
map.put("1", "a");
map.put("2", "b");
// 读和写操作
for (String key : map.keySet()) {
String value = map.get(key);
doSomething(key, value);
}
map.put("3", "c");
3. BlockingQueue:阻塞队列,常用于生产者消费者模式,所谓阻塞是指当队列满了或空了之后的等待策略。常用的有ArrayBlockingQueue、LinkedBlockingQueue等。示例:
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 生产者
new Thread(() -> {
for (int i = 0; i < 10; i++) {
queue.put("a" + i); // 如果队列满了则阻塞
}
}).start();
// 消费者
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
String take = queue.take(); // 如果队列空了则阻塞
doSomething(take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
4. CountDownLatch:用于一个线程等待其他线程完成各自的工作后再执行。关键方法是await()和countDown()。示例:
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
doSomething();
latch.countDown();
}).start();
new Thread(() -> {
doSomething();
latch.countDown();
}).start();
new Thread(() -> {
doSomething();
latch.countDown();
}).start();
latch.await(); // 等待3个线程完成工作
doSomethingElse(); // 3个线程完成后执行
7. 线程池
Java线程池是Java中管理线程的重要方式。主要有以下几个作用:
1. 限制系统中线程的数量,防止线程过多导致资源抢占问题。
2. 重用线程,避免线程频繁创建和销毁带来的性能开销。
3. 提高响应速度,当有新任务到来时,无需创建新线程就可以执行。
4. 提高线程使用效率,可以根据系统负载自动调整池中线程数量。
Java中的线程池主要有以下几种:
1. newCachedThreadPool: creating new threads as needed,but will reuse previously constructed threads when they are available.无限线程池,可以无限创建新线程,适用于短期异步任务。
2. newFixedThreadPool:the pool will only have a fixed number of threads. 固定大小线程池,线程数不变,超出的任务会在队列中等待。
3. newScheduledThreadPool: for scheduling tasks that run periodically or after a delay. 定时任务调度线程池。
4. newSingleThreadExecutor:uses only a single worker thread to execute tasks. 单线程化的线程池,保证任务串行执行。
使用ThreadPoolExecutor可以更细致的控制线程池:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
) {
// ...
}
参数解读:
corePoolSize:线程池中核心线程数最大值
maximumPoolSize:线程池中线程最大数
workQueue:用于储存等待执行的任务的队列
threadFactory:用于设置创建新线程的工厂
handler:线程池的饱和策略
Java线程池提供了一种线程重用和管理机制,适当使用可以提高应用程序效率和性能。
8. 生产者消费者模式
生产者消费者模式适用于解决两个线程间的同步通信问题。生产者线程生产数据,消费者线程消费数据,它们之间需要协调通信,避免数据丢失或重复消费。
Java提供的线程同步方案可以很好地实现生产者消费者模式。可以使用Condition实现生产者消费者模式。
public class ProducerConsumer {
private Lock lock = new ReentrantLock();
private Condition producerCondition = lock.newCondition();
private Condition consumerCondition = lock.newCondition();
private int buffer = 0;
private final int MAX_BUFFER = 10;
public void produce() {
lock.lock();
try {
while (buffer == MAX_BUFFER) {
producerCondition.await();
}
buffer++;
System.out.println("produce one, buffer is " + buffer);
consumerCondition.signal();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (buffer == 0) {
consumerCondition.await();
}
buffer--;
System.out.println("consume one, buffer is " + buffer);
producerCondition.signal();
} finally {
lock.unlock();
}
}
}
分析:
1. 生产者线程调用produce()方法,当buffer已满时await,等待消费者消费。buffer没满则生产一个,并signal通知消费者。
2. 消费者线程调用consume()方法,当buffer为空时await,等待生产者生产。buffer不为空则消费一个,并signal通知生产者。
3. 等待和通知通过Condition实现,而锁通过Lock来控制访问buffer的互斥。
4. 生产者和消费者通过signal/await实现协调通信,避免数据丢失或超生产。
5. buffer起到队列的作用,MAX_BUFFER是队列容量。
这就是一个典型的基于Java线程同步方案实现的生产者消费者模式示例。
9. 并发工具类
Java并发包java.util.concurrent下提供了许多方便的并发工具类,CountDownLatch、CyclicBarrier、Semaphore等。
9.1. CountDownLatch
允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
主要方法:
- countDown():计数减1。
- await():等待,直到计数为0,再继续执行。
public class CountDownLatchDemo {
public void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(() -> {
System.out.println("Child thread 1 execute");
latch.countDown();
});
es.execute(() -> {
System.out.println("Child thread 2 execute");
latch.countDown();
});
latch.await();
System.out.println("Main thread continue");
es.shutdown();
}
}
9.2. CyclicBarrier
一组线程互相等待,直到到达某个公共屏障点。主要方法:
- await():等待,直到所有的线程都到达barrier状态再继续执行。
public class CyclicBarrierDemo {
public void test() throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(3);
ExecutorService es = Executors.newFixedThreadPool(3);
es.execute(() -> {
try {
barrier.await();
System.out.println("Thread 1 continue");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
es.execute(() -> {/*...*/});
es.execute(() -> {/*...*/});
es.shutdown();
}
}
9.3. Semaphore
控制同时访问某个资源的线程数量。主要方法:
- acquire():获取一个许可,若无许可可用则等待。
- release():释放一个许可。
public class SemaphoreDemo {
public void test() throws InterruptedException {
Semaphore semaphore = new Semaphore(2);
ExecutorService es = Executors.newFixedThreadPool(4);
es.execute(() -> {
try {
semaphore.acquire();
System.out.println("Thread 1 running");
Thread.sleep(500);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
/*...*/
es.shutdown();
}
}
9.4. Exchanger
用于线程间交换数据。主要方法:
- exchange():交换数据,并返回对方线程提供的数据。
public class ExchangerDemo {
public void test() {
Exchanger<String> exchanger = new Exchanger<>();
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(() -> {
try {
String data1 = "Hello";
String data2 = exchanger.exchange(data1);
System.out.println(Thread.currentThread().getName() +
" get data: " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.execute(() -> {
try {
String data2 = "World";
String data1 = exchanger.exchange(data2);
System.out.println(Thread.currentThread().getName() +
" get data: " + data1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.shutdown();
}
}
运行结果:
pool-1-thread-1 get data: World
pool-1-thread-2 get data: Hello
分析:
1. 两个线程通过Exchanger的exchange()方法交换数据。
2. 第一个线程将"Hello"交换出去,得到"World"。第二个线程将"World"交换出去,得到"Hello"。
3. 两个线程通过exchange()同步达成数据交换。
4. Exchanger非常适用于两个线程直接交换数据的场景。
9.5. Executor和ExecutorService
用于异步执行任务。主要方法:
- execute():执行一个Runnable任务。
- submit():提交一个Callable任务,并返回Future。
- shutdown():关闭,不再接受新任务。
1. Executor示例:
public class ExecutorDemo {
public void test() {
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
System.out.println("Hello from thread: " +
Thread.currentThread().getName());
});
}
}
运行结果:
Hello from thread: pool-1-thread-1
2. ExecutorService示例:
public class ExecutorServiceDemo {
public void test() throws Exception {
ExecutorService es = Executors.newFixedThreadPool(2);
Future<String> f1 = es.submit(() -> {
return "Task1";
});
Future<String> f2 = es.submit(() -> {
return "Task2";
});
System.out.println(f1.get());
System.out.println(f2.get());
es.shutdown();
}
}
运行结果:
Task1
Task2
3. ScheduledExecutorService示例:
public class ScheduledExecutorServiceDemo {
public void test() throws Exception {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(2);
ses.schedule(() -> {
System.out.println("Hello from thread: " +
Thread.currentThread().getName());
}, 3, TimeUnit.SECONDS);
ses.scheduleAtFixedRate(() -> {
System.out.println("Running...");
}, 1, 2, TimeUnit.SECONDS);
}
}
运行结果:
Running...
Hello from thread: pool-1-thread-1
Running...
Running...
9.6. BlockingQueue
阻塞队列,当队列满时,生产者线程会被阻塞;当队列空时,消费者线程会被阻塞。常用实现类有:
- ArrayBlockingQueue:基于数组的阻塞队列。
- LinkedBlockingQueue:基于链表的阻塞队列。
- PriorityBlockingQueue:支持优先级排序的阻塞队列。
1. ArrayBlockingQueue示例:
public class ArrayBlockingQueueDemo {
public void test() throws Exception {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(() -> {
try {
queue.put("a");
queue.put("b");
queue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.execute(() -> {
try {
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.shutdown();
}
}
运行结果:
a
b
c
2. LinkedBlockingQueue示例:
public class LinkedBlockingQueueDemo {
public void test() throws Exception {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(() -> {
try {
queue.put("a");
queue.put("b");
queue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.execute(() -> {
try {
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
es.shutdown();
}
}
运行结果同上。
3. PriorityBlockingQueue示例:
public class PriorityBlockingQueueDemo {
public void test() throws Exception {
// 使用默认比较器,元素为Integer
BlockingQueue<Integer> queue1 = new PriorityBlockingQueue<>();
queue1.put(3);
queue1.put(1);
queue1.put(4);
System.out.println(queue1.take()); // 1
System.out.println(queue1.take()); // 3
System.out.println(queue1.take()); // 4
// 使用自定义比较器,元素为String
Comparator<String> cmp = (o1, o2) -> o2.length() - o1.length();
BlockingQueue<String> queue2 = new PriorityBlockingQueue<>(cmp);
queue2.put("c");
queue2.put("bb");
queue2.put("aaa");
System.out.println(queue2.take()); // aaa
System.out.println(queue2.take()); // bb
System.out.println(queue2.take()); // c
}
}
运行结果:
1
3
4
aaa
bb
c
分析:
1. PriorityBlockingQueue使用元素的自然排序顺序或者构造时传入的比较器进行排序。
2. queue1使用Integer默认排序,所以输出1, 3, 4。
3. queue2使用字符串长度比较器,所以输出aaa, bb, c。
4. PriorityBlockingQueue适用于需要从队列获取最高优先级元素的场景。
9.7. Future和FutureTask
代表异步计算的结果。主要方法:
- cancel():取消异步任务。
- isDone():判断任务是否完成。
- get():获取结果,如果尚未完成则等待。
9.8. ThreadFactory
用于创建新线程的工厂接口。
这些并发工具类实现了各种并发设计模式,可以简化并发程序的开发过程,提高效率和质量。
10. 并发设计模式
主要有Immutable模式、线程封闭模式、管程模式、 Balking模式、线程池模式等。
10.1. Immutable模式
指对象一旦创建,其状态就不能再改变。这种对象可以在多线程环境下安全共享,典型例子是String。实现方式是:- 将类声明为final,禁止继承
- 将所有字段定义为private final类型
- 不提供set方法
- 在构造方法中初始化所有字段
- 确保构造方法不会导致对象被修改
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
10.2. 线程封闭模式
指一个类对其可变状态实施封闭控制,只允许在一个线程内修改。这可以确保线程安全,实现方式是通过 synchronized 锁定代码段或使用 Java 的线程同步工具类来实现线程封闭。
public class ThreadConfinedExample {
private StringBuilder sb = new StringBuilder();
public void add(String s) {
synchronized (this) {
sb.append(s);
}
}
public String getContents() {
synchronized (this) {
return sb.toString();
}
}
}
10.3. 管程模式
将访问共享资源的权限集中在一段可以被多个线程访问的代码中。通过进程内的同步機制确保一次只有一个线程可以执行这段代码。可通过 synchronized, ReentrantLock 等实现。
public class MonitorExample {
public void doSomething() {
synchronized (this) {
// do something
}
}
}
这个简单示例使用synchronized关键字实现了管程模式。
主要逻辑是:
1. 定义doSomething()方法,该方法对对象this加锁。
2. 在锁定的代码段内执行某些操作。
3. 该方法实现了管程模式,它将访问共享资源的权限(即锁)集中在一段代码内,并确保一次只有一个线程可以执行这段代码。
4. 外部线程调用该方法时,会自动获取this的锁,获得执行权限,然后释放锁让其他线程执行。
这样设计的好处是:
1. 简单易用,通过锁定方法即可实现管程模式。
2. 效率较高,不需要对所有方法都加锁,只需要对关键方法加锁。
3. 易于维护,管程模式将同步逻辑集中在指定方法内,清晰易读。
4. 可重用,管程方法可以被多个线程安全调用和执行。
管程模式的局限是:
1. 如果管程方法内锁定代码段过大,会导致锁竞争和线程饥饿等问题。
2. 存在单一管程方法成为性能瓶颈的风险,降低了并发度。
所以,管程模式适用于同步逻辑较简单,方法粒度较大的场景。对于较复杂的场景,可以使用更加灵活的同步方式,同时也要避免管程方法过于庞大,这需要在并发度和性能之间权衡。但作为一种简单的线程同步实现方式,管程模式还是非常实用的。
管程模式和线程封闭模式都是简单实用的线程安全实现方式,但相比而言,管程模式的同步范围更大,更适合较大粒度的方法同步场景。而线程封闭模式的同步范围更加精细,更适合场景复杂,状态变化频繁的情况。二者可以根据实际需要灵活选择和运用。
10.4. Balking 模式
当一个线程进行某操作时,如果该操作的执行条件不再满足,那么该操作就不再执行,这就是 Balking 行为。实现 Balking 行为的方式是对相关操作加锁,在获取锁之后再检查执行条件是否满足,如果不满足则释放锁。
public class BalkingExample {
public void doSomething() {
boolean changed = false;
synchronized (this) {
if (changed) {
return;
}
// do something
changed = true;
}
}
}
10.5. 线程池模式
为了减少在创建和销毁线程上所消耗的时间以及系统资源之消耗,通常创建一定数量的线程放入池中,这些线程可以被多任务重用,处理不同的客户端请求。线程池主要作用是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.execute(new Runnable() {
public void run() {
// do something
}
});
}
executor.shutdown();
这个简单示例使用Java的Executor框架实现了线程池模式。
主要逻辑是:
1. 使用Executors.newFixedThreadPool(10)创建一个固定大小为10的线程池。
2. 有100个任务需要执行,通过executor.execute()方法提交到线程池。
3. 线程池会自动分配空闲线程执行这些任务,如果当前没有空闲线程,任务会等待在池中的队列里。
4. 通过executor.shutdown()方法关闭线程池。
这样设计的好处是:
1. 重用线程,避免频繁创建和销毁线程的性能开销。
2. 可控制线程数量,避免大量线程竞争导致的性能问题。
3. 提高系统资源利用率,通过等待队列灵活调度线程执行任务。
4. 简化编程模型,通过提交任务到线程池即可,框架自动完成调度和执行。
线程池模式的局限是:
1. 需针对不同场景选择合适的线程池类型和参数,否则可能导致资源浪费或性能瓶颈。
2. 存在任务提交速度过快,线程池来不及处理导致任务积压的风险。需要合理设置等待队列大小和拒绝策略。
3. 线程池模式增加了系统的复杂性,需要深入理解其工作机制,方能合理使用。
所以,线程池模式适用于大量短期任务的处理场景,它可以显著提高系统资源利用率和处理效率。但需要合理配置线程池参数和任务提交策略,避免出现资源浪费或饱和的问题。
线程池模式是一种更高级的线程管理方式,相比直接创建和管理线程,它可以有效隐藏线程层级的细节,极大提高编程效率和系统性能。多线程并发程序设计时,线程池是一种常用而强大的工具。