JDK并发包
说明:
本文内容来自java高并发程序设计(第二版),仅仅为了学习时加深记忆。
多线程的团队协作:同步控制
关键字synchronized的功能扩展:重入锁
在JDK5.0的早期版本中,重入锁的性能远优于关键字sychronized,但从JDK6.0开始,JDK在关键字sychronized上做了大量优化,使得两者性能差距并不大。
下面是一段简单的重入锁的简单案例:
public class ReenterLock implements Runnable{
public static ReentrantLock lock =new ReentrantLock();
public static int i=0;
@Override
public void run() {
for (int j=0;j<10000000;j++){
lock.lock();
try {
i++;
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock tl = new ReenterLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
该段代码利用重入锁来保证对i操作的线程安全。
从该段代码看出,重入锁对逻辑的灵活控制远远优于sychronized。
需要注意的是,在退出临界区(操作共享变量的地方)时,必须记得释放锁,否则将不会有线程获得该锁。
为什么叫重入锁,是因为拥有这把锁的线程可以反复进入该锁,如果不允许这种操作,那么同一个线程在第二次获得锁时,将会和自己产生死锁。
需注意的是:如果一个线程多次获取锁,那么在释放锁的时候,也应该释放相同次数。如果释放次数多了,将会得到java.lang.IllegalMonitorStateException异常。反之,其他线程无法获取该锁。
中断响应
对应sychronized关键字来说,如果一个线程在等待锁,那么结果只有两种情况,获得锁线程执行,得不到锁继续等待。而重入锁提供了第三种可能,根据需要可以取消对锁的请求。
下面代码产生一个死锁,但是得益于中断,我们可以轻易的解决死锁:
public class IntLock implements Runnable {
public static ReentrantLock lock1=new ReentrantLock();
public static ReentrantLock lock2=new ReentrantLock();
int lock;
/**
* 控制加锁顺序,方便构造死锁
* @param lock
*/
public IntLock(int lock){
this.lock=lock;
}
@Override
public void run() {
try {
if(lock==1){
lock1.lockInterruptibly();
Thread.sleep(500);
lock2.lockInterruptibly();
}else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getName()+":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1,"线程1");
Thread t2 = new Thread(r2,"线程2");
t1.start();
t2.start();
Thread.sleep(1000);
//中断其中一个线程
t2.interrupt();
}
执行上述代码会得到以下结果:
中断后线程双双退出,但真正完成工作的只有t1,而t2则放弃任务直接退出,释放资源。
锁申请等待限时
除了等待外部通知之外,要避免死锁的另外一种方法,就是限时等待。
下面这段代码展示了限时等待锁的使用:
public class TimeLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
try {
if(lock.tryLock(5, TimeUnit.SECONDS)){
Thread.sleep(6000);
}else{
System.out.println(Thread.currentThread().getName()+" get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
Thread t1 = new Thread(timeLock,"thread1");
Thread t2 = new Thread(timeLock,"thread2");
t1.start();
t2.start();
}
}
trylock()方法接收两个参数,一个表示等待时长,另一个表示计时单位,如果超过时间还没有得到锁,将返回false。如果成功,则返回true。
在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒内得到锁,因此会请求锁失败。
tryLock()方法可以不带参数直接运行,这种情况下,当前线程试图获得锁,申请成功会返回true,失败则直接返回false,不会引起线程等待。
下面演示这种使用方式:
public class TryLock implements Runnable {
public static ReentrantLock lock1=new ReentrantLock();
public static ReentrantLock lock2=new ReentrantLock();
int lock;
public TryLock(int lock){
this.lock=lock;
}
@Override
public void run() {
if(lock==1){
while (true){
if(lock1.tryLock()){
try{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(lock2.tryLock()){
try {
System.out.println(Thread.currentThread().getName()+" My Job done");
return;
}finally {
lock2.unlock();
}
}
}finally {
lock1.unlock();
}
}
}
}else{
while (true){
if(lock2.tryLock()){
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(lock1.tryLock()){
try {
System.out.println(Thread.currentThread().getName()+" My Job done");
return;
}finally {
lock1.unlock();
}
}
}finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock tryLock1 = new TryLock(1);
TryLock tryLock2 = new TryLock(2);
Thread thread1 = new Thread(tryLock1,"t1");
Thread thread2 = new Thread(tryLock2,"t2");
thread1.start();
thread2.start();
}
}
上述代码,就是让t1获得 lock1,再让t2获得lock2,接着反向请求,让t1申请lock2,t2申请lock1,一般情况下这种会造成死锁。
但是使用tryLock()方法后,这种情况就变了,线程不是傻傻的等待,而是不停的尝试,因此只要执行足够长的时间,线程总会得到所需要的的资源。
以下是输出结果:
公平锁
公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。
如果我们使用sychronized关键字进行控制,那么产生的锁就是非公平锁。公平锁的构造函数如下:
public ReentrantLock(boolean fair)
当参数为true时,是公平锁。公平锁看似优美,但是实现公平锁必须要维护一个有序队列,其实现成本比较高,性能非常低下。所以默认情况下,锁是非公平的。
以下代码可以很好地突出公平锁的特点:
public class FairLock implements Runnable{
public static ReentrantLock lock=new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 获得锁");
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
FairLock fairLock = new FairLock();
Thread t1 = new Thread(fairLock, "t1");
Thread t2 = new Thread(fairLock, "t2");
t1.start();
t2.start();
}
}
上述代码运行结果可以看到,两个线程交替获得锁,输出内容,很有规律。
如果变成非公平锁,会发现情况完全不同。
重入锁的好搭档:Condition
Object.wait()方法和Object.notify()方法是与sychronized关键字合作使用的,而condition是与重入锁相关联的。通过lock接口的Condition newConditon()方法可以生成一个与重入锁绑定的Condition实例。
Condition接口提供的基本方法如下:
- await()方法会使当前线程等待,同时释放当前锁,当其它线程中使用signal()方法或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和object.wait()相似
- awaitUninterruptibly()方法与await()方法基本相同,但它并不会在等待过程中响应中断。
- singal()方法用于唤醒一个在等待中的线程,singalAll()方法会唤醒所有在等待中的线程。
下面代码简单的演示了Condition的功能:
public class ReenterLockCondition implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static Condition condition= lock.newCondition();
@Override
public void run() {
try {
lock.lock();
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition tl = new ReenterLockCondition();
Thread t1 = new Thread(tl);
t1.start();
Thread.sleep(5000);
lock.lock();
condition.signal();
lock.unlock();
}
}
当线程使用Condition.await()方法时,要求线程持有相关的重入锁,在Condition.await()调用时会释放该锁。同理,在singal()方法调用时,也要求线程获得相关锁。在singal()方法调用后,系统会从Condition对象的等待队列中唤醒一个线程,一旦线程唤醒,它会尝试重新获得与之绑定的重入锁,一旦成功获取了,就可以继续执行了。
允许多个线程同时访问:信号量(Semaphore)
信号量为多线程协作提供了更为强大的控制方法。信号量是多锁的扩展,无论是synchronized还是重入锁一次都只允许线程访问一个资源,而信号量却可以指定多个线程,同时访问一个资源。
信号量主要提供了如下构造方法:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
在构造信号量时,必须指定信号量的准入数,即同时能申请多少个许可。
信号量主要的逻辑方法有:
public void acquire();
public void acquireUninterruptibly();
public boolean tryAcquire() ;
public boolean tryAcquire(long timeout, TimeUnit unit);
public void release() ;
acquire()方法尝试获取一个准入许可。若无法获取,则线程会等待,直到有线程释放一个许可或当前线程被中断。acquireUninterruptibly()和acquire()类似,但不响应中断。 tryAcquire()方法尝试获取一个许可,失败则返回false,成功返回true。release() 方法用于在线程访问资源结束后释放一个许可,以使其他等待许可的线程进行资源访问。
下面是一个有关信号量使用的简单实例:
public class SemaphoreDemo implements Runnable {
final Semaphore semp=new Semaphore(5);
@Override
public void run() {
try {
semp.acquire();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getId()+":done");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semp.release();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
for (int i=0;i<20;i++){
executorService.submit(semaphoreDemo);
}
}
}
在本例中同时开启20个线程,观察这段程序的输出,会发现系统以5个线程一组为单位,依次输出带有线程ID的提示文本。
ReadWriteLock读写锁
ReadWriteLock是JDK5中提供的读写分离锁,读写分离锁可以有效的减少锁的竞争,提升系统性能。
A1、A2、A3进行写操作,B1、B2、B3进行读操作,如果使用synchronized或者重入锁,这些操作都必须串行执行,而读操作并没有对数据的完整性造成破坏,当B1读时,B2、B3就需要等待,这是不合理的。因此读写锁就派上用场了。
- 读读不互斥,读读之间不阻塞
- 读写互斥,读会阻塞写,写也会阻塞读
- 写写互斥,写写阻塞
如果在系统中,读操作远远多于写操作,那么读写锁就可以发挥最大的功效。
以下案例说明了读写锁对性能的帮助:
public class ReadWriteLockDemo {
private static Lock lock=new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
private static Lock readLock=readWriteLock.readLock();
private static Lock writeLock=readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
return value;
}finally {
lock.unlock();
}
}
public void handleWrite(Lock lock,int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);
value=index;
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable=new Runnable(){
@Override
public void run() {
try {
// demo.handleRead(readLock);
demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable=new Runnable(){
@Override
public void run() {
try {
// demo.handleWrite(writeLock,new Random().nextInt());
demo.handleWrite(lock,new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i=0;i<18;i++){
new Thread(readRunnable).start();
}
for(int i=0;i<18;i++){
new Thread(writeRunnable).start();
}
}
}
上面代码用普通的重入锁代替读写锁,所有的读和写线程之间必须互相等待,程序执行长达20多秒。
倒计数器:CountDownLatch
CountDownLatch是一个非常实用的多线程控制工具类,顾名思义,它可以让,某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch的构造函数接收一个整数作为参数,即当前这个计数器的计数个数。
public CountDownLatch(int count)
以下这个简单的示例,演示了CountDownLatch的使用:
public class CountDownLatchDemo implements Runnable{
static final CountDownLatch end=new CountDownLatch(10);
static final CountDownLatchDemo demo=new CountDownLatchDemo();
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println("check complete");
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for(int i=0;i<10;i++){
exec.submit(demo);
}
//等待检查
end.await();
System.out.println("Fire!");
exec.shutdown();
}
}
end.countDown()方法,也就是通知CountDownLatch,一个线程完成了任务,倒计时器减一。 end.await();要求主线程等待所有检查任务全部完成,待10个任务全部完成后,主线程才继续执行。
循环栅栏:CyclicBarrier
CyclicBarrier功能和CountDownLatch类似,但它的功能却比CountDownLatch更加强大。
CyclicBarrier可以理解为循环栅栏,也就是这个计数器可以反复使用。比如,我们将计数器设置为10,那么凑齐第一批10个线程后,计数器归零,接着凑齐下一批线程。
CyclicBarrier可以接受一个参数作为barrierAction,barrierAction是指当计数器一次计数完成后,系统会执行的动作,如下构造函数,其中parties表示计数总数。
public CyclicBarrier(int parties, Runnable barrierAction)
如下示例演示了司令命令士兵完成任务的场景:
public class CyclicBarrierDemo {
public static class Soldier implements Runnable{
private String soldier;
private final CyclicBarrier cyclic;
Soldier(CyclicBarrier cyclic,String soldierName){
this.cyclic=cyclic;
this.soldier=soldierName;
}
@Override
public void run() {
try {
//等待所有士兵到齐
cyclic.await();
doWork();
//等待所有士兵完成工作
cyclic.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
private void doWork() {
try {
Thread.sleep(Math.abs(new Random().nextInt()%10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soldier+":任务完成");
}
}
public static class BarrierRun implements Runnable{
boolean flag;
int N;
public BarrierRun(boolean flag,int N){
this.flag=flag;
this.N=N;
}
@Override
public void run() {
if(flag){
System.out.println("司令:[士兵"+N+"个,任务完成!]");
}else {
System.out.println("司令:[士兵"+N+"个,集合完毕!]");
flag=true;
}
}
}
public static void main(String[] args) {
int N=10;
Thread[] allSoldier=new Thread[N];
boolean flag=false;
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
//设置屏障点,主要是为了执行这个任务
System.out.println("集合队伍");
for (int i=0;i<N;i++){
System.out.println("士兵"+i+"报道!");
allSoldier[i]=new Thread(new Soldier(cyclic,"士兵"+i));
allSoldier[i].start();
}
}
}
线程阻塞工具类:LockSupport
LockSupport是一个非常方便的线程阻塞工具,它可以在线程内任意位置让线程阻塞。
LockSupport的静态方法park()可以阻塞当前线程,以下是个例子:
public class LockParkDemo {
public static Object u=new Object();
public static ChangeObjectThread t1=new ChangeObjectThread("t1");
public static ChangeObjectThread t2=new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread{
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
synchronized (u){
System.out.println("in "+getName());
LockSupport.park();
System.out.println("out "+getName());
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
不太懂书中的解释:
这是因为LockSupport类使用类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么park()方法会立刻返回,并且消费这个许可,如果许可不可用就会阻塞,而unPark()方法则使得一个许可变为可用(但和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)
除了有定时阻塞功能外,LockSupport.park()还支持中断影响,但是和其他接受中断的函数不同,它不会抛出InterruptedException异常,它只会默默返回,下个例子说明:
在这里插入代码片