《实战Java高并发程序设计》读书笔记(二):JDK并发包

第三章 JDK并发包

3.1 多线程的团队协作:同步控制

同步控制是并发程序必不可少的重要手段。

1、关键字synchronized的功能扩展:重入锁

重入锁可以完全替代关键字synchronized。重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。

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 < 100000; j++) {
            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
            }
        }
    }
}

与关键字synchronized相比,重入锁有着显示的操作过程。必须手动指示何时加锁,何时释放锁。因此,重入锁对逻辑控制的灵活性要远远优于synchronized。

重入锁是可以反复进入的,一个线程可以多次获得同一把锁,但是要记得释放相同次数。

重入锁的高级功能:

(1)中断响应

对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么获得这把锁继续执行,要么保持等待。而重入锁则使得线程在等待锁的时候可以被中断。这种情况对于处理死锁是有一定帮助的。

public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int 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().getId()+"线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();t2.start();
        Thread.sleep(1000);
        t2.interrupt();//中断t2
    }
}

线程t1和t2启动后,由于互相请求对方持有的锁造成死锁。如果不中断t2,那么会无限期等待下去。中断t2后,t2会放弃对lock1的申请,同时释放已获得的lock2,从而使得t1获得lock2顺利执行下去。

中断后两个线程都会退出,但真正完成工作的只有t1。

(2)锁申请等待限时

除了等待外部通知以外,要避免死锁还有另一种方法:限时等待。可以使用tryLock() 方法进行一次限时的等待。

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("get Lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLock t1 = new TimeLock();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t1);
        thread1.start();
        thread2.start();
    }
}

tryLock() 方法接受两个参数,等待时长和计时单位。由于占用锁的线程会持有锁长达6秒,故另一个线程无法在5秒之内获得锁,请求失败。

ReentrantLock.tryLock()不带参数时,线程会尝试获得锁,申请成功返回true,申请失败则会立即返回false,不在进行等待。

(3)公平锁

使用synchronized关键字进行锁控制时,产生的锁是非公平的,就是说会在等待队列中随机挑选一个获得锁。

重入锁允许对公平性进行设置,构造函数如下:

public ReentrantLock(boolean fair)

当参数为true时,表示锁是公平的。公平锁按照时间先后顺序,保证不会产生饥饿现象。但是实现成本较高,性能比较地下,默认状况下为非公平的。

2、重入锁的好搭档:Condition

与Object.wait() 和notify() 作用大致相同。

public class ReenterLockCondition implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    //通过newCondition方法生成一个与当前重入锁绑定的Condition实例。
    public static Condition condition = lock.newCondition();

    @Override
    public void run() {

        try {
            lock.lock();
            //await方法会使当前线程等待在Condition上,同时释放当前锁
            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 reenterLockCondition = new ReenterLockCondition();
        Thread thread = new Thread(reenterLockCondition);
        thread.start();
        Thread.sleep(2000);
        //在signal方法调用时,要求线程先获得相关的锁
        lock.lock();
        //调用signal方法,系统会从当前Condition对象的等待队列中唤醒一个线程
        //同理,signalAll方法会唤醒所有线程
        condition.signal();
        //在signal方法调用后,需要释放相关的锁,让给被唤醒的线程
        //若没有释放锁,线程thread无法继续执行
        lock.unlock();
    }
}

3、允许多个线程同时访问:信号量(Semaphore)

信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程,同时访问一个资源。

//信号量的构造函数,构造时必须制定准入数,即同时能申请多少个许可
public Semaphore(int permits)
public Semaphore(int permits,boolean fair)  //第二个参数可以指定是否公平

//信号量的方法
public void acquire()  //尝试获得一个准入许可
public void acquireUninterruptibly()  //不响应中断
public boolean tryAcquire()  //尝试获得一个许可,成功返回true,失败返回false,不等待
public boolean tryAcquire(long timeout, TimeUnit unit)  //等待限时
public void release()  //用于访问结束后释放一个许可

一个简单的例子:

public class SemapDemo implements Runnable {
    //声明一个包含5个许可的信号量
    final Semaphore semp = new Semaphore(5);

    @Override
    public void run() {
        try {
            //申请信号量
            semp.acquire();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId()+":done!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //离开前要释放信号量
            semp.release();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemapDemo demo = new SemapDemo();
        //同时开启20个线程,但每次只能有5个线程进行输出
        for (int i = 0; i < 20; i++) {
            exec.submit(demo);
        }
    }
}

4、ReadWriteLock读写锁

读写分离锁能有效地帮助减少锁竞争,提升系统性能。

一般来说,读与读之间不互斥,可以并行操作;读与写、写与写之间仍会互相阻塞。

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 readRunable=new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(readLock);
//                    demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable writeRunable =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(readRunable).start();
        }
        for (int i = 18; i < 20; i++) {
            new Thread(writeRunable).start();
        }
    }
}

以上程序使用读写锁时2秒多就能执行完, 因为读操作是并行的,而使用重入锁时需要20多秒(读操作也会阻塞)。

5、倒计数器:CountDownLatch

通常用来控制线程等待,可以让某一个线程等待直到倒计数器结束再开始执行。

public class CountDownLatchDemo implements Runnable {
    //创建一个倒计数器的实例,参数为计数器的计数个数
    //参数为10表示需要10个线程完成任务后等待在CountDownLatch上的线程才能继续执行
    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("go!");
        exec.shutdown();
    }
}

6、循环栅栏:CyclicBarrier

类似CountDownLatch,但功能更强大。比如,把计数器设置为10,那么在凑齐第一批10个线程后,一起开始执行任务,直到任务完成,然后计数器就会归零,接着凑齐下一批10个线程。

7、线程阻塞工具类:LockSupport

可以在线程内任意位置让线程阻塞。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值