AQS同步组件(二)

java中锁主要分为两类: 1、 synchronized 关键字修饰的锁   2、 在同步容器JUC中 ReentrantLock(可重入性) 关键字修饰的锁

ReenTrantLock 和 synchronized 的区别: 

    1、可重入性 ,两者都是一样的,当有线程进入锁,计数器就加1,当计数器为0的时候,释放锁  

    2、 锁的实现 ,Synchronized 锁是基于jvm实现的 ,而ReenTrantLock是基于JDK实现的

   3、性能的区别 ,在synchronized进行性能优化之后,两者之间的区别是很小的,更建议使用synchronized,因为实现起来更为简单

    4、 功能的区别:  synchronized使用起来更加的方便和便捷,是由JVM实现加锁的,而reenTrantLock是需要通过代码实现加锁机制的 ,在锁的细粒度和灵活性方面reentrantLock是由于Synchronized的

   ReenTrantLock独有的功能: 

     1、 ReenTrantLock 可以指定是公平锁还是非公平锁 ,synchronized只能是非公平锁 ,所谓的公平锁就是先等待的线程先获得锁

      2、提供了一个condition类,可以实现分组唤醒需要唤醒的线程

    3、 提供了能够中断等待锁的线程的机制,lock.lockinterruptibly

ReenTrantLock 代码示例:  

   

package MyStudyTest.LOck;

import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@ThreadSafe
public class LockExample1 {
    //总请求数
    private static int clientTotal = 5000;

    //总的并发请求数
    private static int threadTotal = 200;

    private static int count = 0;

    private final static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        //创建一个动态可变长线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal) ;
        Semaphore sema = new Semaphore(threadTotal);

        for (int i =0; i<clientTotal; i++ ){
            executorService.execute(()->{
                try {
                    sema.acquire();
                    add();
                    sema.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
        }
    private  static void add() {
        lock.lock();
        try {
            count++;
        }finally{
            lock.unlock();
        }
    }

    }

关于ReenTrantLock 中一些方法的使用: (提供了很多线程操作的函数 )

    1、 trylock ()    --: 仅当线程调用时锁定未被另一个线程调用的情况下才获取锁定

    2、lockinteruptibly : 只有当当前线程没有被中断的情况下,才获取锁定 ,如果线程中断了,就抛出异常

    ReenTrantReadWriteLock : 当没有任何读写锁的时候,才获取锁

 

stampedLock: 是乐观锁,所谓乐观锁是指当读的操作很多但是写的操作很少的时候,默认读写是互不影响的,因此不悲观的使用完全的读取锁定。 可以提高程序的吞吐量

 乐观锁和悲观锁使用源码实例:

  

package com.mmall.concurrency.example.lock;

import java.util.concurrent.locks.StampedLock;

public class LockExample4 {

    class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();

        void move(double deltaX, double deltaY) { // an exclusively locked method
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }

        //下面看看乐观读锁案例
        double distanceFromOrigin() { // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }

        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
                while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) { //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else { //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }
}

 

ReenTrantLock 的condition 方法的使用:

   代码实例: 

  

package com.mmall.concurrency.example.lock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class LockExample6 {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(() -> {
            try {
                reentrantLock.lock();
                log.info("wait signal"); // 1
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("get signal"); // 4
            reentrantLock.unlock();
        }).start();

        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock"); // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal ~ "); // 3
            reentrantLock.unlock();
        }).start();
    }
}

 

condition是AQS中等待队列之一,是多线程间协调通信的工具类,必须等待某个条件成熟之后,线程才会被唤醒执行。

 对上述代码1-4步骤的解释:

  1、首先代码调用了reenTrantLock的lock方法,则将线程加入到了AQS的等待队列中。当调用了condition.await()方法之后,线程就从AQS的等待队列中移除了,相当于进行了锁的释放,然后将线程加入到了Condition 的等待队列中。

  2、因为上述的线程一释放了锁,所以线程2就获得了锁,然后执行发送信号的方法condition.signall(),因为condition 中有线程1的节点,于是就将线程1取出放入到AQS的等待队列中,然后线程2调用了释放锁的方法,唤醒线程1,线程1被唤醒之后,继续执行,然后释放锁。

  

 FutureTask 是JUC中的,但不是AQS的子类:

  Callable和Runnable 接口对比:

  Future接口:

  FutureTask类:

 

关于Future 类的代码示例:

 

package com.mmall.concurrency.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Slf4j
public class FutureExample {

    static class MyCallable implements Callable<String> {

        @Override
        public String call() throws Exception {
            log.info("do something in callable");
            Thread.sleep(5000);
            return "Done";
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        String result = future.get();
        log.info("result:{}", result);
    }
}

关于futureTask 的代码示例:

  

package com.mmall.concurrency.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

@Slf4j
public class FutureTaskExample {

    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.info("do something in callable");
                Thread.sleep(5000);
                return "Done";
            }
        });

        new Thread(futureTask).start();
        log.info("do something in main");
        Thread.sleep(1000);
        String result = futureTask.get();
        log.info("result:{}", result);
    }
}

 

Fork/join 框架: 是JDK7 中提供的用于定型执行任务的框架,是一个将大人物分割成小人物,等执行完成之后,将小人物汇总成一个大人物的框架 。

采用的是工作窃取的算法,工作窃取算法是指一个线程从其他队列中窃取任务进行执行的算法。

 将一个大人物分割成多个小任务,然后每个小任务对应一个线程,为了使每个任务之间保持自身的独立性,将每个线程放入到一个独立的队列中执行。

使用了一个双端队列,减少线程之间的竞争。

 Fork/join执行机制的限制: 任务只能使用Fork和join来进行任务的执行。

Fork/join 代码示例:

  

package com.mmall.concurrency.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {

    public static final int threshold = 2;
    private int start;
    private int end;

    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);

            // 执行子任务
            leftTask.fork();
            rightTask.fork();

            // 等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();

            // 合并子任务
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();

        //生成一个计算任务,计算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);

        //执行一个任务
        Future<Integer> result = forkjoinPool.submit(task);

        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

 

阻塞队列: 阻塞队列主要用在生产者和消费者的场景。

当一个线程想要加入队列的时候,如果这时队列已经满了,就会对该线程进行阻塞,直到一个线程执行了出队列的操作。

如果一个线程先要对一个队列执行出队列的操作,而这时这个队列已经是空的,那么这个线程就会被阻塞,直到一个线程执行了入队列的操作 。

 所以说阻塞队列是线程安全的。

负责生产的线程会不断的向这个队列中插入数据,直到这个队列满时,这个线程会被阻塞。

负责消费的线程会不断的从这个队列中消费数据,直到这个队列为空时,这个线程会被阻塞。

 

blockQueue的一些实现类:

   1、 ArrayBlockingQueue: 是一个有界的阻塞队列,主要特点是他内部实现是一个数组,必须在其初始化的时候指定容量的大小,这个大小一旦被指定了之后,就不能发生变化了。

    2、用先进先出的方式处理数据 。

 DelayQueue: 阻塞的是内部元素,该队列需要继承一个delay接口,实现元素的有序性。

LinkedBlockQueue: 大小边界是可以进行选择的,如果指定了大小边界,那么他的大小就是固定的,如果不指定就是可变的。

 PriorityblockQueue: 大小是无边界的,但是有排序规则,可以将null插入到队列中。 所有插入PriorityBlockQueue的元素都是需要实现comparable接口的,

 SynchronousQueue: 内部仅允许一个元素,当一个线程插入一个元素后,就会被阻塞,除非这个元素被另一个线程消费,因此这个队列又叫同步队列,是一个无界非缓存的队列,

 

 

 

 

 

 

 

 

 

 

 

 

 

   

 

posted @ 2019-09-13 15:37  青紫天涯  阅读(247)  评论(0编辑  收藏  举报