2.2、多线程(下)
2.2.9、线程池是如何实现线程复用的?
答:
线程池采用了生产者-消费者模型来实现线程复用。
提交任务到线程池里的线程被称为生产者线程,它不断往线程池里传递任务,这些任务会被保存到线程池的阻塞队列里。然后线程池里的工作线程会不断从阻塞队列中获取任务并执行。基于阻塞队列的特性,如果阻塞队列中没有任务时,这些工作线程就会阻塞等待,直到又有新的任务进来,这些工作线程再次被唤醒,从而达到线程复用的目的。
扩展:
- 核心线程复用: 线程池会维护一定数量的核心线程,这些线程在处理任务时会一直保持存活,不会被回收。当有新任务提交时,线程池会优先选择空闲的核心线程来执行任务,避免频繁地创建和销毁线程。
- 工作队列: 如果核心线程都在执行任务,而新任务继续到达,线程池就会将这些任务放入工作队列。工作队列可以存放一定数量的任务,当核心线程忙碌时,新任务会先进入工作队列等待执行。
- 非核心线程复用: 如果工作队列已满,而线程池中的线程数量未达到最大线程数,线程池会创建新的非核心线程来处理任务。这些非核心线程在执行完任务后,如果在一定时间内没有新任务到达,可能会被回收。
2.2.10、线程池如何知道一个线程的任务已经执行完成?
答:
从两个方面来说:
- 线程池内部:当我们把一个任务交给线程池执行,线程池会调度工作线程来执行这个任务的run()方法,run()方法正常结束后,会调用afterExecute(task, thrown)这个钩子方法,线程池中的工作线程调用了这个钩子方法就说明任务执行完成了。
- 线程池外部:当我们想在线程池外部获取线程池内部任务的执行状态,有以下几种方式:
- 使用submit()方法提交任务时,该方法会返回一个Future对象。通过调用Future对象的isDone()方法判断任务是否执行完成。
示例: ExecutorService executorService = Executors.newFixedThreadPool(5); Future<String> future = executorService.submit(new MyTask()); boolean isDone = future.isDone();
- 使用execute()方法提交任务时,execute()方法没有返回值,无法直接知道任务是否执行完成。但可以通过将Runnable任务对象转换为FutureTask对象,然后调用其isDone()方法判断任务是否执行完成。
示例: ExecutorService executorService = Executors.newFixedThreadPool(5); FutureTask<String> futureTask = new FutureTask<>(new MyTask(), null); executorService.execute(futureTask); boolean isDone = futureTask.isDone(); 在线程池的任务执行完之前,future.get()方法会一直阻塞,直到任务执行结束。 所以,只要future.get()方法正常返回,就意味着传入线程池中的任务已经执行完成了。
- 重写钩子方法afterExecute(),这是工作线程执行完成以后会回调的方法,可以重写这个方法来获取工作线程的执行结果。
- 使用submit()方法提交任务时,该方法会返回一个Future对象。通过调用Future对象的isDone()方法判断任务是否执行完成。
2.2.11、wait和notify为什么要写在synchronized代码块中?
答:
在多线程中,要实现多个线程之间的通信,除了管道流,就只能通过共享变量的方式来实现了,也就是线程t1修改了共享变量S,线程t2获取的必须是修改后的共享变量S,从而完成数据通信,所以,就必须要有一个synchronized同步锁这样的一个互斥条件,参与通信的线程必须要竞争到这个共享变量的锁资源才能修改共享变量。
为了避免wait/notify方法的错误使用,JDK强制要求把wait/notify方法写在同步代码块里,否则会抛出IllegalMonitorStateException。
扩展:
在Java中,synchronized关键字用于实现线程之间的同步,而wait和notify方法则是synchronized关键字的一部分,用于线程之间的通信。
wait方法使当前线程进入等待状态,直到其他线程调用notify或notifyAll方法唤醒它。notify方法用于唤醒等待中的线程之一,而notifyAll方法用于唤醒所有等待中的线程。
示例:
class SharedData {
private int number;
private boolean flag = false;
public synchronized void produce(int value) {
while (flag) { // 如果flag为true,表示已经有生产好的数据,等待消费者消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number = value; // 生产数据
System.out.println("生产者生产了:" + value);
flag = true; // 标记已经有生产好的数据
notify(); // 唤醒等待中的消费者线程
}
public synchronized void consume() {
while (!flag) { // 如果flag为false,表示还没有生产好的数据,等待生产者生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了:" + number);
flag = false; // 标记消费者已经消费了数据
notify(); // 唤醒等待中的生产者线程
}
}
class Producer implements Runnable {
private SharedData sharedData;
public Producer(SharedData sharedData) {
this.sharedData = sharedData;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
sharedData.produce(i); // 生产数据
}
}
}
class Consumer implements Runnable {
private SharedData sharedData;
public Consumer(SharedData sharedData) {
this.sharedData = sharedData;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
sharedData.consume(); // 消费数据
}
}
}
public class Main {
public static void main(String[] args) {
SharedData sharedData = new SharedData();
Producer producer = new Producer(sharedData);
Consumer consumer = new Consumer(sharedData);
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
在上面的示例中,SharedData类是一个共享数据类,其中有一个生产者方法produce和一个消费者方法consume。
生产者在生产数据后调用wait方法来释放锁并进入等待状态,消费者在消费数据后调用notify方法来唤醒等待中的生产者线程。
这样就实现了生产者和消费者之间的通信,确保生产者生产完数据后再由消费者消费。
注意到,在produce和consume方法中都使用了synchronized关键字来保证同步访问共享数据。
2.2.12、wait方法和sleep方法的区别有哪些?
答:
在Java中,wait()方法和sleep()方法都可以用于线程的暂停执行,但是它们有一些重要的区别。
- wait()方法是Object类的方法,而sleep()方法是Thread类的静态方法。这意味着任何对象都可以调用wait()方法,而sleep()方法只能由线程对象调用。
- wait()方法会释放当前线程持有的对象的锁,并且使当前线程进入等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法唤醒它。而sleep()方法只是暂停当前线程的执行,不会释放任何锁。wait()方法和sleep()方法都会释放CPU资源。
- wait()方法必须在synchronized代码块中调用,否则会抛出IllegalMonitorStateException异常。而sleep()方法可以在任何地方调用。
- wait()方法可以由其他线程唤醒,而sleep()方法只能等待指定的时间后自动唤醒。
总的来说,wait()方法主要用于线程间的协作,用于等待其他线程的通知,而sleep()方法主要用于线程的暂停执行一段时间。
2.2.13、volatile关键字有什么用?
答:
- 线程可见性:使用volatile关键字修饰的变量,会保证多个线程之间对该变量的修改可见。也就是说,一个线程对volatile变量的修改,会立即被其他线程看到。
- 禁止指令重排序:volatile关键字会禁止指令重排序,保证被volatile修饰的变量的读写操作按照指令顺序执行。
- 不保证原子性:尽管volatile关键字能够保证多个线程之间对变量的可见性,但对于复合操作(例如自增、自减)并不保证原子性。如果需要保证原子性操作,可以考虑使用java.util.concurrent.atomic包下的原子类。
需要注意的是,虽然volatile关键字能够保证可见性,但在某些情况下,使用volatile并不能完全替代锁的功能。在需要保证原子性和一致性的场景下,仍然需要使用synchronized或Lock等机制来进行控制。
2.2.14、ThreadLocal的作用是什么?
答:
ThreadLocal在Java中的作用是为每个线程提供一个独立的变量副本,使得每个线程在使用变量时都能够独立地改变其副本的值,而不会影响其他线程的副本。这样就可以解决多线程环境下共享变量的并发访问问题。
ThreadLocal实现了线程与变量绑定的机制,每个线程都可以通过ThreadLocal对象来访问自己的变量副本。线程之间互不干扰,相互之间的变量副本是隔离的。这种机制可以简化多线程环境下共享变量的同步控制,并提高程序的运行效率。
ThreadLocal常用于存储线程私有的上下文信息,如用户身份、请求参数等。在使用线程池或异步任务等场景下,可以避免传递参数的复杂性,同时保证线程安全和性能。
通过ThreadLocal的get()和set()方法可以获取和设置线程私有的变量副本,remove()方法可以移除当前线程对应的变量副本,避免内存泄漏。
示例:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建并启动两个线程
Thread thread1 = new Thread(() -> {
threadLocal.set(10); // 设置当前线程的变量值为10
System.out.println("Thread1: " + threadLocal.get()); // 获取当前线程的变量值
threadLocal.remove(); // 移除当前线程的变量值
});
Thread thread2 = new Thread(() -> {
threadLocal.set(20); // 设置当前线程的变量值为20
System.out.println("Thread2: " + threadLocal.get()); // 获取当前线程的变量值
threadLocal.remove(); // 移除当前线程的变量值
});
thread1.start();
thread2.start();
}
}
运行结果:
Thread1: 10
Thread2: 20
在这个示例中,我们创建了一个ThreadLocal对象 threadLocal,用于存储整数类型的变量副本。
在两个线程中,分别通过threadLocal.set()方法设置当前线程的变量值,然后使用threadLocal.get()方法获取当前线程的变量值,并打印出来。
最后通过threadLocal.remove()方法移除当前线程对应的变量副本。
总之,ThreadLocal在多线程环境下提供了一种简单而有效的线程隔离机制,是处理线程间共享变量的一种重要工具。
2.2.15、多个线程之间是如何实现数据共享的?
答:
有以下几种方式:
- 共享对象:创建一个类,其中包含可以被多个线程访问的字段。
- 使用volatile关键字:确保字段的修改对所有线程都是可见的。
- 使用ThreadLocal:为每个线程提供共享变量的独立副本。
- 使用synchronized块或方法:确保同一时刻只有一个线程可以访问代码块。
- 使用Atomic类:提供原子性的操作,如AtomicInteger等。
- 使用BlockingQueue:实现线程间的数据传递。
1.共享对象:
public class SharedObject {
public int sharedData;
public SharedObject(int sharedData) {
this.sharedData = sharedData;
}
}
public class SharedObjectExample {
public static void main(String[] args) {
SharedObject sharedObject = new SharedObject(0);
// 创建两个线程,共享同一个对象
Thread thread1 = new Thread(() -> {
sharedObject.sharedData = 10;
});
Thread thread2 = new Thread(() -> {
System.out.println(sharedObject.sharedData);
});
thread1.start();
thread2.start();
}
}
运行结果:
10
2.使用volatile关键字:
public class VolatileExample {
public static volatile int sharedData = 0;
public static void main(String[] args) {
// 创建两个线程,共享同一个volatile变量
Thread thread1 = new Thread(() -> {
sharedData = 10;
});
Thread thread2 = new Thread(() -> {
System.out.println(sharedData);
});
thread1.start();
thread2.start();
}
}
运行结果:
10
3.使用ThreadLocal:
public class ThreadLocalExample {
public static ThreadLocal<Integer> sharedData = new ThreadLocal<>();
public static void main(String[] args) {
// 创建两个线程,每个线程都有独立的sharedData副本
Thread thread1 = new Thread(() -> {
sharedData.set(10);
});
Thread thread2 = new Thread(() -> {
System.out.println(sharedData.get());
});
thread1.start();
thread2.start();
}
}
运行结果:
null
4.使用synchronized块或方法:
public class SynchronizedExample {
public static int sharedData = 0;
public static synchronized void increment() {
sharedData++;
}
public static void main(String[] args) {
// 创建两个线程,共享同一个方法
Thread thread1 = new Thread(() -> {
increment();
});
Thread thread2 = new Thread(() -> {
increment();
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sharedData);
}
}
运行结果:
2
5.使用Atomic类:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
public static AtomicInteger sharedData = new AtomicInteger(0);
public static void main(String[] args) {
// 创建两个线程,共享同一个AtomicInteger变量
Thread thread1 = new Thread(() -> {
sharedData.incrementAndGet();
});
Thread thread2 = new Thread(() -> {
sharedData.incrementAndGet();
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sharedData.get());
}
}
运行结果:
2
6.使用BlockingQueue:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueExample {
public static BlockingQueue<Integer> sharedData = new LinkedBlockingQueue<>();
public static void main(String[] args) {
// 创建两个线程,共享同一个BlockingQueue
Thread thread1 = new Thread(() -> {
try {
sharedData.put(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
System.out.println(sharedData.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
运行结果:
10
标签:Thread,编程,线程,sharedData,new,2.2,多线程,方法,public
From: https://blog.csdn.net/weixin_61769871/article/details/142098517