zoukankan      html  css  js  c++  java
  • 第3章 JDK并发包(一)

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

    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 < 1000000; 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,确保多线程对i操作的安全性。从这段代码可以看到,与synchronized相比,重入锁有着明显的操作过程。开发人员必须手动指定何时加载,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远好于synchronized。在退出临界区时,必须记得释放锁,否则,其他线程就没有机会再访问临界区了。
    • 重入锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程。上述代码可以写成下面的形式:
    lock.lock();
    lock.lock();
    try {
        i++;
    } finally {
        lock.unlock();
        lock.unlock();
    }
    
    • 在这种情况下,一个线程连续两次获得同一锁。这是允许的!如果不允许这么操作,那么同一个线程在第2次获得锁时,将会和自己产生死锁。程序就会“卡死”在第2次申请锁的过程中。但需要注意的是,如 果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。如果释放锁的次数多,那么会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。
    • 重入锁可以提供中断处理的能力。
    • 中断响应
    • 对于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();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedExcepiton e) {}
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {}
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                e.printStaceTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (loc2.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();
        }
    }
    
    • 线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1.因此,很容易形成t1和t2之间的相互等待。在这里,对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响 应中断。

    • 锁申请等待限时

    • 除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。给定一个等待时间,让线程自动放弃。可以使用tryLock()方法进行一次限时的等待。

    public class TimeLock implements Runnable {
        public static ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            try {
                //超过5秒还没得到锁,返回false
                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 t1 = new Thread(tl);
            Thread t2 = new Thread(tl);
            t1.start();
            t2.start();
        }
    }
    
    • tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。
    • ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回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) {}
                            if (lock2.tryLock()) {
                                try {
                                    System.out.println(Thread.currentThread().getId() + ":My Job done");
                                    return;
                                } finally {
                                    lock2.unlock();
                                }
                            }
                        } finally {
                            lock1.unlock();
                        }
                    }
                }
            } else {
                while (true) {
                    if (lock2.tryLock()) {
                        try {
                            try {
                                Thread.sleep(500);
                            } catch (InterruptedException e) {}
                            if (lock1.tryLock()) {
                                try {
                                    System.out.println(Thread.currentThread().getId() + ":My Job done");
                                    return;
                                } finally {
                                    lock1.unlock();
                                }
                            }
                        } finally {
                            lock2.unlock();
                        }
                    }
                }
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            TryLock r1 = new TryLock(1);
            TryLock r2 = new TryLock(2);
            Thread t1 = new Thread(r1);
            Thread t2 = new Thread(r2);
            t1.start();
            t2.start();
        }
    }
    
    • 上述代码中,采用了非常容易死锁的加锁顺序。也就是先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,t2申请lock1.在一般情况下,这会导致t1和t2互相等待,从而引起死锁。

    • 但使用tryLock()后,线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行。

    • 公平锁

    • 在大多数情况下,锁的申请都是非公平的。系统只是会从这个锁的等待队列中随机挑选一个。

    • 公平锁的一大特点是:它不会产生饥饿现象。

    • 如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:

    public ReentrantLock(boolean fair)
    
    • 参数为true时,表示锁是公平的。实现公平锁要求系统维护一个有序队列,因此实现成本比较高,性能也相对低下。因此默认情况下,锁是非公平的。
    public class FairLock implements Runnable {
        public static ReentrantLock fairLock = new ReentrantLock(true); //指定锁是公平的
        
        @Override
        public void run() {
            while (true) {
                try {
                    fairLock.lock();
                    System.out.println(Thread.currentThread().getName() + " 获得锁");
                } finally {
                    fairLock.unlock();
                }
            }
        }
        
        public static void main(String[] args) {
            FairLock r1 = new FairLock();
            Thread t1 = new Thread(r1, "Thread_t1");
            Thread t2 = new Thread(r1, "Thread_t2");
            t1.start();t2.start();
        }
    }
    
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    
    • 从输出结果来看,两个线程基本上是交替获得锁的。
    • 下面是使用非公平锁时的部分输出:
    Thread_t1 获得锁
    Thread_t1 获得锁
    Thread_t1 获得锁
    Thread_t1 获得锁
    Thread_t1 获得锁
    Thread_t1 获得锁
    Thread_t2 获得锁
    Thread_t2 获得锁
    Thread_t2 获得锁
    Thread_t2 获得锁
    Thread_t2 获得锁
    Thread_t2 获得锁
    
    • 可以看到,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。
    • 对上面ReentrantLock的几个重要方法整理如下。
      • lock():获得锁,如果锁已经被占用,则等待。
      • lockInterruptibly():获得锁,但优先响应中断。
      • tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
      • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
      • unlock():释放锁
    • 就重入锁的实现来看,它主要集中在Java层面。在重入锁的实现中,主要包含三个要素:
      • 第一,是原子状态。原子状态使用CAS操作来存储当前锁的状态,判定锁是否已经被别的线程持有。
      • 第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
      • 第三,是阻塞语句park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
  • 相关阅读:
    【BZOJ4538】[Hnoi2016]网络 整体二分+树状数组
    【BZOJ4543】[POI2014]Hotel加强版 长链剖分+DP
    【BZOJ1304】[CQOI2009]叶子的染色 树形DP
    【BZOJ4552】[Tjoi2016&Heoi2016]排序 二分+线段树
    【BZOJ4557】[JLoi2016]侦察守卫 树形DP
    【BZOJ4499】线性函数 线段树
    【BZOJ1576】[Usaco2009 Jan]安全路经Travel 最短路+并查集
    【BZOJ4560】[JLoi2016]字符串覆盖 KMP+状压DP
    【BZOJ2124】等差子序列 树状数组维护hash值
    MDX导航结构层次:《Microsoft SQL Server 2008 MDX Step by Step》学习笔记九
  • 原文地址:https://www.cnblogs.com/sanjun/p/8319949.html
Copyright © 2011-2022 走看看