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接口提供的基本方法如下:

  1. await()方法会使当前线程等待,同时释放当前锁,当其它线程中使用signal()方法或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和object.wait()相似
  2. awaitUninterruptibly()方法与await()方法基本相同,但它并不会在等待过程中响应中断。
  3. 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异常,它只会默默返回,下个例子说明:

在这里插入代码片
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值