通过之前几章的学习已经知道在线程间通信用到的synchronized关键字、volatile关键字以及等待/通知(wait/notify)机制。今天讲一下线程间通信的其他知识点:管道输入/输出流、Thread.join()的使用、ThreadLocal的使用。并学习Lock接口。
一、管道输入/输出流
管道输入/输出流和普通文件的输入/输出流或者网络输入、输出流不同之处在于管道输入/输出流主要用于线程之间的数据传输,而且传输的媒介为内存。
管道输入/输出流主要包括下列两类的实现:
面向字节: PipedOutputStream、 PipedInputStream
面向字符: PipedWriter、 PipedReader
二、Thread.join()的使用
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。另外,一个线程需要等待另一个线程也需要用到join()方法。
Thread类除了提供join()方法之外,还提供了join(long millis)、join(long millis, int nanos)两个具有超时特性的方法。这两个超时方法表示,如果线程thread在指定的超时时间没有终止,那么将会从该超时方法中返回。
2.1 join方法使用
1 | public class Test { |
2 | |
3 | public static void main(String[] args) throws InterruptedException { |
4 | |
5 | MyThread threadTest = new MyThread(); |
6 | threadTest.start(); |
7 | |
8 | //Thread.sleep(?);//因为不知道子线程要花的时间这里不知道填多少时间 |
9 | System.out.println("我想当threadTest对象执行完毕后我再执行"); |
10 | } |
11 | static public class MyThread extends Thread { |
12 | |
13 | |
14 | public void run() { |
15 | System.out.println("我想先执行"); |
16 | } |
17 | |
18 | } |
19 | } |
假如子线程运行的结果被主线程运行需要怎么办? sleep方法? 当然可以,但是子线程运行需要的时间是不确定的,所以sleep多长时间当然也就不确定了。这里就需要使用join方法解决上面的问题。
1 | public class Test { |
2 | |
3 | public static void main(String[] args) throws InterruptedException { |
4 | |
5 | MyThread threadTest = new MyThread(); |
6 | threadTest.start(); |
7 | |
8 | //Thread.sleep(?);//因为不知道子线程要花的时间这里不知道填多少时间 |
9 | threadTest.join(); |
10 | System.out.println("我想当threadTest对象执行完毕后我再执行"); |
11 | } |
12 | static public class MyThread extends Thread { |
13 | |
14 | |
15 | public void run() { |
16 | System.out.println("我想先执行"); |
17 | } |
18 | |
19 | } |
20 | } |
加上了一句:threadTest.join();。在这里join方法的作用就是主线程需要等待子线程执行完成之后再结束。
2.2 join(long millis)方法的使用
join(long millis)中的参数就是设定的等待时间。
另外threadTest.join(2000) 和Thread.sleep(2000) 和区别在于: Thread.sleep(2000)不会释放锁,threadTest.join(2000)会释放锁 。
三、ThreadLocal的使用
变量值的共享可以使用public static变量的形式,所有线程都使用一个public static变量。 如果想实现每一个线程都有自己的共享变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
再举个简单的例子: 比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。
3.1 InheritableThreadLocal
ThreadLocal类固然很好,但是子线程并不能取到父线程的ThreadLocal类的变量,InheritableThreadLocal类就是解决这个问题的。
在使用InheritableThreadLocal类需要注意的一点是:如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的还是旧值。
四、Lock接口
4.1 Lock接口简介
锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。
在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。
Lock是synchronized关键字的进阶,掌握Lock有助于学习并发包中的源代码,在并发包中大量的类使用了Lock接口作为同步的处理方式。
Lock接口的实现类: ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
4.2 Lock的简单使用
1 | Lock lock=new ReentrantLock(); |
2 | lock.lock(); |
3 | try{ |
4 | }finally{ |
5 | lock.unlock(); |
6 | } |
因为Lock是接口所以使用时要结合它的实现类,另外在finall语句块中释放锁的目的是保证获取到锁之后,最终能够被释放。
注意: 最好不要把获取锁的过程写在try语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁无法被释放。
4.3 Lock接口提供的synchronized关键字不具备的主要特性
尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁:在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回
4.4 Lock接口基本的方法:
方法名称 | 描述 |
---|---|
void lock() | 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。 |
void lockInterruptibly() | 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
boolean tryLock() | 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。 |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false. |
void unlock() | 释放锁。 |
五、Lock接口的实现类:ReentrantLock
ReentrantLock和synchronized关键字一样可以用来实现线程之间的同步互斥,但是在功能是比synchronized关键字更强大而且更灵活。
当一个线程运行完毕后才把锁释放,其他线程才能执行,其他线程的执行顺序是不确定的。
5.1 Condition接口简介
通过之前的学习知道了:synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。
Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。
而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
5.2 使用Condition实现等待/通知机制
在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。
注意: 必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。
使用Condition实现顺序执行:在一个线程运行完之后通过condition.signal()/condition.signalAll()方法通知下一个特定的运行运行,就这样循环往复即可。
5.3 公平锁与非公平锁
Lock锁分为:公平锁 和 非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。
公平锁的运行结果是有序的。
把Service的参数修改为false则为非公平锁
1 | final Service service = new Service(false);//true为公平锁,false为非公平锁 |
非公平锁的运行结果是无序的。
六、ReadWriteLock接口的实现类:ReentrantReadWriteLock
6.1简介
ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。
读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
6.2 ReentrantReadWriteLock的特性与常见方法
ReentrantReadWriteLock的特性:
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量上来看还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁也能够同时获取读锁 |
锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级称为读锁 |
ReentrantReadWriteLock常见方法: 构造方法
方法名称 | 描述 |
---|---|
ReentrantReadWriteLock() | 创建一个 ReentrantReadWriteLock()的实例 |
ReentrantReadWriteLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantReadWriteLock的实例 |
6.3 ReentrantReadWriteLock的使用
读读共享
1 | |
2 | private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); |
3 | |
4 | public void read() { |
5 | try { |
6 | try { |
7 | lock.readLock().lock(); |
8 | System.out.println("获得读锁" + Thread.currentThread().getName() |
9 | + " " + System.currentTimeMillis()); |
10 | Thread.sleep(10000); |
11 | } finally { |
12 | lock.readLock().unlock(); |
13 | } |
14 | } catch (InterruptedException e) { |
15 | // TODO Auto-generated catch block |
16 | e.printStackTrace(); |
17 | } |
18 | } |
两个线程同时运行read方法,你会发现两个线程可以同时或者说是几乎同时运行lock()方法后面的代码,输出的两句话显示的时间一样。这样提高了程序的运行效率。
写写互斥
把上面的代码的
1 | lock.readLock().lock(); |
改为:
1 | lock.writeLock().lock(); |
两个线程同时运行read方法,你会发现同一时间只允许一个线程执行lock()方法后面的代码
读写互斥
1 | private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); |
2 | |
3 | public void read() { |
4 | try { |
5 | try { |
6 | lock.readLock().lock(); |
7 | System.out.println("获得读锁" + Thread.currentThread().getName() |
8 | + " " + System.currentTimeMillis()); |
9 | Thread.sleep(10000); |
10 | } finally { |
11 | lock.readLock().unlock(); |
12 | } |
13 | } catch (InterruptedException e) { |
14 | e.printStackTrace(); |
15 | } |
16 | } |
17 | |
18 | public void write() { |
19 | try { |
20 | try { |
21 | lock.writeLock().lock(); |
22 | System.out.println("获得写锁" + Thread.currentThread().getName() |
23 | + " " + System.currentTimeMillis()); |
24 | Thread.sleep(10000); |
25 | } finally { |
26 | lock.writeLock().unlock(); |
27 | } |
28 | } catch (InterruptedException e) { |
29 | e.printStackTrace(); |
30 | } |
31 | } |
1 | |
2 | Service service = new Service(); |
3 | |
4 | ThreadA a = new ThreadA(service); |
5 | a.setName("A"); |
6 | a.start(); |
7 | |
8 | Thread.sleep(1000); |
9 | |
10 | ThreadB b = new ThreadB(service); |
11 | b.setName("B"); |
12 | b.start(); |
运行两个使用同一个Service对象实例的线程a,b,线程a执行上面的read方法,线程b执行上面的write方法。你会发现同一时间只允许一个线程执行lock()方法后面的代码。记住:只要出现写操作的过程就是互斥的。
6.4 写读互斥
记住:只要出现写操作的过程就是互斥的。
参考: