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