美文网首页
Java多线程--JDK并发包(1)

Java多线程--JDK并发包(1)

作者: sunhaiyu | 来源:发表于2018-04-25 09:25 被阅读29次

    Java多线程--JDK并发包(1)

    之前介绍了synchronized关键字,它决定了额一个线程是否可以进入临界区;还有Object类的wait()notify()方法,起到线程等待和唤醒作用。synchronized关键字经常和它们一起使用,因为wait和notify在调用之前需要获得“锁”,而锁时依靠synchronized获得的。

    同步机制

    重入锁

    下面是一个使用ReentrantLock的简单例子。可以看到,和synchronized相比,必须手动调用unlock()方法释放资源。

    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReEntryLock {
        public static class Demo implements Runnable {
            public static ReentrantLock lock = new ReentrantLock();
            public static int i = 0;
    
            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    lock.lock();
                    try {
                        i++;
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    
    }
    
    

    顾名思义,重入就是一个线程可以反复进入同一把锁,注意仅限于一个线程。下面的例子演示了一个个线程两次获得一把锁。

    lock.lock();
    lock.lock();
    try{
        i++;
    } finally {
        lock.unlock();
        lock.unlock();
    }
    
    

    有一点要注意,获得几次锁,释放时也放释放对应的次数,不能多释放,否则会抛出异常;少释放的话相当于该线程还持有着锁,其他线程依然不能进入临界区。

    中断响应

    对于synchronized,若一个线程在等待锁,要么:

    • 拿到锁,继续执行;
    • 继续等待。

    重入锁可以响应中断lockInterruptibly()是一个可以对中断进行响应的锁申请动作。当线程调用interrupt()方法后,正在等待锁的线程可以接收到这个通知,被告知不用再等待,可以停止了。线程对中断的响应对处理死锁有一定帮助

    有等待时限的锁申请

    在申请锁的时候使用tryLock(5, TimeUnit.SECONDS)就表示该线程最多等待5s,5s后还得不到就会返回false表示放弃对锁的等待。tryLock()还可以没有参数,此时如果锁没有被其他线程占用就立即返回true, 否则立即返回false,不会等待,可以认为是传入了时间0s。使用该方法可以改善死锁和饥饿的情况。

    公平锁

    所谓公平锁,即按照线程进入等待队列时间的先后顺序,先来的可以先得到锁。公平锁不会产生饥饿,只要排队,总有个时候轮到你。

    synchronized进行锁控制产生的锁是不公平的,ReentrantLock有个构造函数,可以传入一个boolean类型的值,设定为true就能产生公平锁

    Condition条件

    wait()notify()\notifyAll()与synchronized关键字搭配使用,condition有await()signal()\singalAll()方法,与重入锁搭配使用。和wait()\notify()一样,await()\signal()也需要在调用之前获得重入锁await()被调用后会释放重入锁。

    Condition是由lock.newCondition()生成的,可见这条语句生成一个和当前重入锁绑定的Condition。

    // a Runnale class
    @Override
    public void run() {
        try {
            lock.lock();
            // 当前线程在condition对象上等待(进入condition的等待队列)
            condition.await();
        } finally {
            lock.unlock();
        }
    }
    
    // in main
    t1.start();
    Thread.sleep(2000);
    lock.lock();
    // main主线程唤醒在condition上等待的t1线程(condition从队列释放一个线程)
    condition.signal();
    lock.unlock();
    

    信号量

    不论是synchronized还是重入锁ReentrantLock,都是一次只能允许一个线程访问一个资源。使用信号量Semaphore可以指定多个线程,同时访问某一个资源。

    public Semaphore(int permits);
    public Semaphore(int permits, boolean fair);
    

    permits参数就决定了信号量能同时申请多少个许可,也就是指定允许多少个线程同时访问一个资源,fair表示是否为公平锁。

    信号量使用起来和lock很像,不同的是允许指定个数的线程进入。举个例子:

    Semaphore s = new Semaphore(5); // 允许同时5个线程进入临界区
    
    @Override
    public void run() {
        s.acquire(); // 有点类似lock.lock();
        System.out.println(Thread.currentThread().getId()); // 和lock不同,这个地方可允许5个线程同时进来
        s.release(); // 有点类似lock.unlock();
    }
    

    其中acquire()尝试获得一个准入的许可,若无法获得会等待;release()则是访问资源结束后释放许可,使得其他线程可以得到这个许可。

    ReadWriteLock读写锁

    读数据并不会对数据造成损坏,所以应允许多个线程同时读取,当时当有线程参与写操作时,应该阻塞——也就是线程之间:

    • 读-读不互斥,不阻塞;
    • 读-写互斥,读会阻塞写,写也会阻塞读;
    • 写-写互斥,写写相互阻塞。

    读写锁在读取次数远大于写次数时,有很好的性能

    倒计时类CountDownLatch

    public CountDownLatch(int count)
    

    接受一个参数count,也就是计数个数。

    public static final CountDownLatch cdl = new CountDowmLatch(10); // 计数个数10
    public static final CountDownLatchDemo demo = new CountDowmLatchDemo();
    // class CountDowmLatchDemo implements Runnable
    @Override
    public void run() {
        try {
            /* some task */
            Thread.sleep(1000);
            cdl.countDown(); // 一个任务已完成,计数器减1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    // in main
    
    ExecutorService exec = Executors.newFixedThreadPool(10); // 固定10个线程的线程池
    
    for (int i = 0; i < 10; i++) {
        exec.submit(demo);// 提交10个任务,也就是开了10个线程
    }
    // 当前的main线程阻塞在了cdl上,检查任务全部任务都完成后,在这里就是等cdl计数10次完毕后才可以继续执行
    cdl.await();
    System.out.println("计时10s终于结束了,该我表演了");
    
    exec.shutDown(); // 别忘了关闭线程池,否则程序不会终止
    

    循环栅栏CyclicBarrier

    这个类也可以完成计数功能,但是比CountDownLatch更强大。这个计数器是可以循环使用的,比如设定了计数10,凑齐了第一批10个线程后,又开始一轮新的计数......

    CyclicBarrier的强大之处在于在构造函数中还可以指定一个实现了Runnable接口的实例,表示每结束一次计数,就执行一次动作。结合循环可以实现强大的功能。比如每十分钟去检查一遍下载进度条...

    线程阻塞工具LockSupport

    该类可以在线程内任意位置让线程阻塞,不像Thread.suspend()那样由于在suspend之前就调用resume而导致的线程永久挂起,也不像wait()方法一样必须获得对象的锁才能调用。主要是park()unpark()方法,即使unpark()park()之前被调用了,也不用担心线程被永久挂起。因为LockSupport使用了类似信号量的机制,它为每个线程都准备了一个许可,若许可可用,park()立即返回,并将许可变成不可用;如果许可不可用,就会阻塞。而unpark()将一个许可变成可用。所以即使unpark()park()之前被调用了,park()方法就立即返回了,这就杜绝了永久挂起的现象。

    park()还能响应中断,且不抛出异常,只是默默返回。


    by @sunhaiyu
    2018.4.23

    相关文章

      网友评论

          本文标题:Java多线程--JDK并发包(1)

          本文链接:https://www.haomeiwen.com/subject/yydelftx.html