线程本地存储
使用synchronized
关键字对整个方法加锁(防止其他线程访问整个方法)往往会带来更大的性能开销,如果你只想保护某些代码块,可以使用同步代码块,这一段被锁保护的代码块就称为临界区( critical section ),前面的显式锁所保护的区域以及使用synchronized保护的代码块都是临界区。
线程本地存储 #
既然共享资源需要考虑同步问题,那么阻止资源共享就可避免线程冲突1。java.lang.ThreadLoacl
类提供了一种机制,为使用相同变量的不同线程提供不同的存储,称为线程本地存储。
考虑SimpleDateFormat
类,它不是线程安全的,如果作为全局变量,在多线程情况下可能会出现问题。使用同步的开销太大,一般是直接使用局部变量来解决问题,不过这也很浪费资源。因为SimpleDateFormat
不必是共享资源,这时候,可以使用线程本地存储:
1public static final ThreadLocal<SimpleDateFormat> dateFormat
2 = ThreadLoacal.withInitial(()->{
3 new SimpleDateFormat("yyyy-MM-dd");
4 })
这样每个线程都有一个dataFormat
实例。
下例中,每个线程都有一个线程本地存储,用于存储一个0-100的随机数,然后对其进行自增运算:
1public class ThreadLocalVariableHolder {
2 // Java 8 提供的方法
3 private static final ThreadLocal<Integer> value =
4 ThreadLocal.withInitial(new Supplier<Integer>() {
5 @Override
6 public Integer get() {
7 Random r = new Random();
8 return r.nextInt(100);
9 }
10 });
11
12 static class Task implements Runnable {
13
14
15 static void increment() {
16 value.set(value.get() + 1);
17 }
18
19 static Integer getValue() {
20 return value.get();
21 }
22
23 @Override
24 public String toString() {
25 return Thread.currentThread() + ": " + getValue();
26 }
27
28 @Override
29 public void run() {
30 while (!Thread.currentThread().isInterrupted()) {
31 increment();
32 System.out.println(this);
33 }
34 }
35 }
36
37 public static void main(String[] args) throws InterruptedException {
38 for (int i = 0; i < 2; i++) {
39 new Thread(new Task()).start();
40 }
41 TimeUnit.MILLISECONDS.sleep(1);
42 System.exit(0);
43 }
44}
45/* output(sample):
46Thread[Thread-1,5,main]: 41
47Thread[Thread-3,5,main]: 19
48Thread[Thread-1,5,main]: 42
49Thread[Thread-3,5,main]: 20
50Thread[Thread-1,5,main]: 43
51Thread[Thread-3,5,main]: 21
52...
53*///:~
可以看到,虽然没有同步,但是也无需担心资源冲突的问题,线程1和线程3的数据不会互相干扰。
ThreadLoacl通常作为静态域存储,虽然多个线程只有一个ThreadLocal实例,但是每个实例都有自己的存储,并不会有竞争条件。
再论Object超类 #
之前的讨论中,我们说到了
Object超类的hashCode
和equals
方法,这次在多线程环境下阐释Object超类的其他几个重要方法。
多线程条件下,使用互斥(mutex)来解决资源共享问题时常用手段,接下来讨论的是如何让2个线程之间协同起来。
其实在可重入锁的
条件对象的使用中,就使用了对象之间的协作——当要转账时,发现余额不足则当前转账线程等待,而被其他线程唤醒以继续执行(虽然它可能又进入等待)。它工作的机制是线程A获得了锁,但是发现其必须在某个条件上等待(余额充足),于是其阻塞并释放锁(可被中断),线程B得以获得锁并执行,B执行完成之后唤醒线程A,其进入Runnable
状态。
线程在条件上等待的工作逻辑
Object对象的wait()
,notify()
和notifyAll()
方法提供了线程线程之间协作的能力。
wait()
方法使当前线程进入等待,其还可以接受一个超时参数。
wait()
方法必须配合synchronized关键字使用,原因是调用wait()
方法时,该对象的监视器被释放了——前提是必须要先持有对象的监视器。
notify()
用于唤醒一个在当前监视器(如果是临界区,则是指定对象锁;若是同步方法,则是实例锁)上等待的线程,notify方法有相当的局限性:
- 并不是唤醒所有的
wait()
线程,它没有这个能力,只能唤醒在相同锁(监视器)上等待的线程; - 并不是唤醒指定当前监视器的线程,它只唤醒一个,至于是哪一个是不确定的;
notifyAll()
用于唤醒在当前监视器上等待的所有线程。
notify()
和notifyAll()
方法也只能在获取锁之后执行,被唤醒的线程也只有等调用notify()
和notifyAll()
方法的锁被释放之后才可能继续执行。
考虑下面的例子:
1public class WaitOnCondition {
2 private volatile boolean tracked = false;
3
4 synchronized void playTrack() throws InterruptedException {
5 if (!tracked) {
6 // 在WaitOnCondition实例上等待
7 wait();
8 }
9 System.out.println("play ");
10 tracked = false;
11 }
12
13 synchronized void recordTrack() {
14 if (tracked) {
15 return;
16 }
17 System.out.println("record ");
18 tracked = true;
19 // 最好不要使用notify,除非你明确地知道期待的线程一定被唤醒
20 notifyAll();
21 }
22
23 class Play implements Runnable {
24
25 @SneakyThrows
26 @Override
27 public void run() {
28 while (true) {
29 playTrack();
30 TimeUnit.MILLISECONDS.sleep(1000);
31 }
32 }
33 }
34
35 class Record implements Runnable {
36 @SneakyThrows
37 @Override
38 public void run() {
39 while (true) {
40 recordTrack();
41 TimeUnit.MILLISECONDS.sleep(1000);
42 }
43 }
44 }
45
46 public static void main(String[] args) throws InterruptedException {
47 WaitOnCondition tp = new WaitOnCondition();
48 var pool = Executors.newCachedThreadPool();
49 pool.submit(tp.new Play());
50 pool.submit((tp.new Record()));
51
52 TimeUnit.SECONDS.sleep(5);
53 System.exit(0);
54 }
55}
56/* output:
57record play record play record play record play
58*///:~
record和play任务本来是可以无序运行的,但是由于play任务在playTrack()
方法上使用了wait()
,条件是布尔值tracked
,该值由record任务在recordTrack
时修改,修改完成之后record任务负责唤醒等待的线程。这样就完成了线程的交互。
将tracked
设置为volatile
变量是volatile关键字的典型应用场景。
在使用条件时,应当谨慎地避免死锁。
有时候资源共享是必须的,同步也是必须的。 ↩︎