线程本地存储

线程本地存储

使用synchronized关键字对整个方法加锁(防止其他线程访问整个方法)往往会带来更大的性能开销,如果你只想保护某些代码块,可以使用同步代码块,这一段被锁保护的代码块就称为临界区critical section ),前面的显式锁所保护的区域以及使用synchronized保护的代码块都是临界区。

线程本地存储 #

既然共享资源需要考虑同步问题,那么阻止资源共享就可避免线程冲突1java.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超类hashCodeequals方法,这次在多线程环境下阐释Object超类的其他几个重要方法。

多线程条件下,使用互斥(mutex)来解决资源共享问题时常用手段,接下来讨论的是如何让2个线程之间协同起来。

其实在可重入锁的 条件对象的使用中,就使用了对象之间的协作——当要转账时,发现余额不足则当前转账线程等待,而被其他线程唤醒以继续执行(虽然它可能又进入等待)。它工作的机制是线程A获得了锁,但是发现其必须在某个条件上等待(余额充足),于是其阻塞并释放锁(可被中断),线程B得以获得锁并执行,B执行完成之后唤醒线程A,其进入Runnable状态。

线程在条件上等待的工作逻辑

线程在条件上等待的工作逻辑

Object对象的wait()notify()notifyAll()方法提供了线程线程之间协作的能力

wait()方法使当前线程进入等待,其还可以接受一个超时参数。

wait()方法必须配合synchronized关键字使用,原因是调用wait()方法时,该对象的监视器被释放了——前提是必须要先持有对象的监视器

notify()用于唤醒一个在当前监视器(如果是临界区,则是指定对象锁;若是同步方法,则是实例锁)上等待的线程,notify方法有相当的局限性:

  1. 并不是唤醒所有的wait()线程,它没有这个能力,只能唤醒在相同锁(监视器)上等待的线程;
  2. 并不是唤醒指定当前监视器的线程,它只唤醒一个,至于是哪一个是不确定的;

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关键字的典型应用场景。

在使用条件时,应当谨慎地避免死锁。



  1. 有时候资源共享是必须的,同步也是必须的。 ↩︎