实战Java高并发程序设计(三)JDK并发包

来源:互联网 发布:印度军工 知乎 编辑:程序博客网 时间:2024/05/22 04:28

实战Java高并发程序设计(三)JDK并发包


  1. 同步控制——重入锁
       重入锁可以完全替代synchronized关键字。其使用方法如下:

    public ReentrantLock lock = new Reentrantlock();            public void run(){                lock.lock();                lock.lock();                try{                    do something...                }finally{//为了保证该线程执行完临界区代码后能释放锁,将unlock放在finally中                    lock.unlock();                    lock.unlock();                }            }

       由于其通过人工进行lock和unlock,因此比synchronized更好控制临界区。
       注意,这段代码有两个lock.lock();,这也是为啥这叫冲入锁的原因,同一个线程可以多次获得锁,但是必须要多次释放该锁,否则其它线程无法进入该临界区。

    • 中断响应
         ReentrantLocklockInterruptibly()方法是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。它对于处理死锁有一定的帮助。
    • 锁申请等待限时
         除了等待外部通知(例如给一个中断)之外,避免锁死还有另外一种方法,那就是限时等待。我们可以使用tryLock()方法进行一次限时等待。

      public static ReentrantLock lock = new ReentrantLock();public void run(){    try{        if(lock.tryLock(5,TimeUnit.SECONDS)){            Thread.sleep(6000);        }else{            System.out.println("get lock failed");        }    }catch(InterruptedException e){        e.printStackTrance();    }finally{        if(lock.isHeldByCurrentThread())//lockInterruptibly()与tryLock()一样,在释放前要判断当前线程是否获得该锁资源。            lock.unlock();    }}

         如果tryLock()方法没有携带任何参数,那么默认不进行等待,这样也不会发生死锁。

    • 公平锁
         它会按照时间先后,保证先到着优先获得该锁,而不考虑其优先级。它的最大特点是,不会产生饥饿现象。而synchronized关键字产生的锁就是非公平的。
         重入锁有一下构造函数:

      public ReentrantLock(boolean fair)

         当fair为true时,表示公平锁。要注意,实现公平锁需要系统维护一个有序队列,因此公平锁的性能相对较低。在非公平锁的情况下,根据系统的调度,一个线程会倾向于再次获得已经持有的锁,即在多个具有相同优先级的线程连续抢占同一把锁时,很容易发生同一个线程连续获得该锁的情况,这种分配方式无疑是高效的,但不公平。

       在重入锁的实现中,主要包含三个要素:
       第一是原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是已经被被的线程持有。
       第二是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队里中唤醒一个线程,继续工作。
       第三是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

  2. Condition条件
       wait()和notify()方法是和synchronized关键字合作使用的,而Condition的await()和signal()是与重入锁相关联的。
       当线程使用Condition.await()时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。同理,在Condition.signal()方法调用时,也要求线程先获得相关的锁。在signal()方法调用后,系统会从当前Condition对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在signal()方法调用之后,还需要释放先关的锁

    public ReentrantLock lock = new ReentrantLock();public Condition condition = lock.newCondition();
  3. 信号量(Semaphore)
       信号量是对锁的扩展,它能够制定多个线程同时访问某一个线程。申请信号量是用acquire()操作,离开临界区时,务必要使用release()释放信号量,否则会导致能进入临界区的线程越来越少,最后所有的线程均不可访问临界区。

  4. 读写锁(ReadWriteLock)
       在一个系统中,读-读不互斥、读-写互斥、写-写互斥,在读操作消耗远高于写消耗的情况下,读写分离能够有效地减少锁竞争,提升系统性能。我们可以通过以下方法来获得读锁(ReadLock)和写锁(WriteLock)。

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();private static Lock readLock = readWriteLock.readLock();//获得读锁private static Lock writeLock = readWriteLock.writeLock();//获得写锁
  5. 计数器(CountDownLatch)
    这个工具通常用来控制线程等待,它可以让某一个线程等待,直到计数结束再执行。比如有4个线程跑4个任务A、B、C、D,D任务需要ABC都完成之后才能执行,此时就能够使用CountDownLatch。一下例子输出4中模拟读写锁的总耗时。

    public class test {    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 static int value = 0;    private static CountDownLatch latch = new CountDownLatch(40);    public void read(Lock lock){        try{            lock.lock();            Thread.sleep(1000);            System.out.println("read:"+value);        }        catch (InterruptedException e) {            e.printStackTrace();        }finally{            lock.unlock();        }    }    public void write(Lock lock,int val){        try{            lock.lock();            value = val;            System.out.println("write:"+value);        }finally{            lock.unlock();        }    }    public static void main(String[] args) throws InterruptedException {        final test t = new test();        Runnable readRunnable = ()->{            t.read(readLock);            latch.countDown();//计数器减一,也代表完成了一个任务        };        Runnable writeRunnable = ()->{            t.write(writeLock, new Random().nextInt(100));            latch.countDown();//计数器减一,也代表完成了一个任务        };        long beg = new Date().getTime();        for(int i = 0 ;i<20;i++){            Thread th = new Thread(readRunnable);            th.start();        }        for(int j = 0 ; j< 20 ;j++){            Thread th = new Thread(writeRunnable);            th.start();        }        latch.await();//如果计数器没到0,则阻塞;当计数器到0时,则继续执行。        System.out.println(System.currentTimeMillis()-beg);    }}
  6. 循环栅栏(CyclicBarrier)
       CyclicBarrier是CountDownLatch的加强版,它能够循环计数,每次计数完成之后,会执行指定的方法。其构造函数如下:

    public CyclicBarrier(int parties, Runnable barrierAction)

   其中parties用来指定线程数量,barrierAction用来指定每次计数完成之后,执行的函数。注意:这个函数由这轮计数,最后一个到来的线程执行。
7. 线程阻塞工具类(LockSupport)
   LockSupport是一个非常方便的线程阻塞工具,它可以在线程内任意位置让线程阻塞。但是它不像suspend那样会导致多个线程死锁,也不像wait和notify那样需要先获得某个对象锁。

> LockSupport.park()方法可以阻塞当前线程> LockSupport.parkNanos(long nanos)能够实现一个限时等待。> LockSupport.parkUntil(long deadline)能够指定等待的最晚时间。&ensp; &ensp;LockSupport使用了类似信号量的机制,它为每一个线程准备了一个许可,如果许可可用,那么park()方法就会立刻返回,否则就会阻塞。而unpark()方法则使得一个许可变为可用。
  1. 线程池
       与进程相比,线程是一种轻量级的工具,但是其创建和关闭依然会花费时间。并且大量的线程会抢占内存资源,也会给GC带来很大压力。因此在实际项目中,线程的数量必须加以控制,盲目地创建线程可能会降低系统性能。
       
阅读全文
0 0
原创粉丝点击