网络知识 娱乐 每日一练进击大厂「DAY8」并发编程5

每日一练进击大厂「DAY8」并发编程5

文章目录

  • 一、ReadWriteLock
  • 二、interrupted和isInterrupted方法的区别
  • 三、Semaphore
  • 四、CyclicBarrier
  • 五、happens-before模型
  • 六、fail-safe和fail-fast
  • 七、阻塞队列的有界和无界
  • 八、分布式锁
  • 九、如何实现幂等
  • 十、线程池如何知道一个线程的任务已经执行完成
  • 总结

一、ReadWriteLock

ReadWriteLock是一个读写锁的接口,ReentrantLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提高了读写的性能。

二、interrupted和isInterrupted方法的区别

Thread.interrupt()来中断一个线程就会设置中断标记为true,当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被标记为false;非静态方法isInterrupted()用来查询其他线程的中断状态且不会改变中断状态标记,任何抛出InterruptedException异常的方法都会将中断状态置为false。
interrupt()通知线程需要结束,如果线程在waiting会抛出InterruptedException,线程自己决定要不要结束,异常会触发复位。
isInsterrupted()获取线程的中断标记,但是不会进行复位操作。
interrupted()获取线程的中断标记,并且主动执行复位操作。

public class InterruptDemo implements Runnable {n @Overriden public void run() {n while (!Thread.currentThread().isInterrupted()) {n try {n Thread.sleep(100);n } catch (InterruptedException e) {n System.out.println("异常里的isInterrupted:" + Thread.currentThread().isInterrupted());n Thread.currentThread().interrupt();//再次中断n System.out.println("再次中断:" + Thread.currentThread().isInterrupted());n }n }n System.out.println("线程结束");n }n public static void main(String[] args) throws InterruptedException {n Thread t1 = new Thread(new InterruptDemo());n t1.start();n Thread.sleep(1000);n t1.interrupt();n }n}nnnpublic class InterruptedDemo {n public static void main(String[] args) {nn Thread.currentThread().interrupt();n //查询中断状态,中断标记被标记为truen System.out.println(Thread.interrupted());n //获取线程的中断状态,本来应该是true,但是interrupted()触发了复位 所以为falsen System.out.println(Thread.currentThread().isInterrupted());nn }n}n

三、Semaphore

public class SemaphoreDemo {n public static void main(String[] args) {n Semaphore semaphore = new Semaphore(5);n for (int i = 0; i < 10; i++) {n new Car(i, semaphore).start();n }n }nn static class Car extends Thread {n private int num;n private Semaphore semaphore;nn public Car(int num, Semaphore semaphore) {n this.num = num;n this.semaphore = semaphore;n }nn public void run() {n try {n semaphore.acquire();//获取一个许可n System.out.println("第" + num + "占用 一个停车位");n TimeUnit.SECONDS.sleep(2);n System.out.println("第" + num + "俩车走喽");n semaphore.release();n } catch (InterruptedException e) {n e.printStackTrace();n }n }n }n}n

四、CyclicBarrier

public class CyclicBarrierDemo extends Thread {n @Overriden public void run() {n System.out.println("开始进行数 据分析");n }nn public static void main(String[] args) {n CyclicBarrier cycliBarrier = new CyclicBarrier(3, new CyclicBarrierDemo());n new Thread(new DataImportThread(cycliBarrier, "file 1")).start();n new Thread(new DataImportThread(cycliBarrier, "file 2")).start();n new Thread(new DataImportThread(cycliBarrier, "file 3")).start();n }n}nnpublic class DataImportThread extends Thread{n private CyclicBarrier cyclicBarrier;n private String path;nn public DataImportThread(CyclicBarrier cyclicBarrier, String path) {n this.cyclicBarrier = cyclicBarrier;n this.path = path;n }nn @Overriden public void run() {n System.out.println("开始导入: "+path+" 位置的数据");n try {n cyclicBarrier.await();//阻塞n } catch (InterruptedException e) {n e.printStackTrace();n } catch (BrokenBarrierException e) {n e.printStackTrace();n }n }n}nn

五、happens-before模型

前者对后者可见

  1. 程序顺序规则:处理器不能对存在依赖关系的操作进行重排序,因为重排序会改变程序的执行结果,对于没有依赖关系的指令,即使重排序,也不会改变在单线程环境下的执行结果。
  2. volatile变量规则:volatile修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作。
  3. 监视器锁:一个线程对于一个锁的释放锁操作,一定happens-before与后续线程对这个锁的加锁操作。
  4. start规则:start()操作之前的操作一定happens-before start()操作。
  5. join规则:join之前的操作的结果,对于join之后的操作可见。
  6. 6)传递性规则(单线程):a happens-before b , b happens-before c , a happens-before c
    a的结果对b可见,b的结果对c可见,那么a的结果对c可见。

六、fail-safe和fail-fast

fail-safe和fail-fast是多线程并发操作集合时的一种失败处理机制。

fail-fast表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常,从而导致遍历失败。
例如:定义一个Map集合,使用Iterator迭代器进行数据遍历,在遍历过程中对集合数据做变更时,就会发生fail-fast。java.util包下的集合类都是快速失败机制的,常见的使用fail-fast方式遍历的容器有HashMap和ArrayList。

fail-safe表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出ConcurrentModificationException,原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历,由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到。

例如:定义一个CopyOnWriteArrayList,在对这个集合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的使用fail-safe方式遍历的容器有ConcurrentHashMap和CopyOnWriteArrayList。

七、阻塞队列的有界和无界

阻塞队列:当队列为空时,获取队列中元素的消费者线程被阻塞,同时唤醒生产者线程,当队列满了,向队列中添加元素的生产者线程被阻塞,同时唤醒消费者线程。ArrayBlockingQueue是基于数组结构的阻塞队列,也就是队列元素是存储在一个数组结构里面,并且由于数组有长度限制,为了达到循环生产和循环消费的目的,ArrayBlockingQueue用到了循环数组。

有界:阻塞队列中能够容纳的元素个数,通常情况下是有界的,ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数,这种就是有界队列。

无界:就是没有设置固定大小的队列,不过并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像LinkedBlockingQueue,它的默认长度是Integer.Max_Value。

八、分布式锁

分布式锁,是一种跨进程的跨机器节点的互斥锁,它可以用来保证多机器节点对于共享资源的排他性。分布式锁和线程锁本质上是一样的,线程锁的生命周期是单进程多线程,分布式锁的生命周期是多进程多机器节点。
二者都满足以下特性:

  • 排他性,也就是说同一时刻只能有一个节点去访问共享资源;
  • 可重入性,允许一个已经获得锁的进程,在没有释放锁之前再次重新获得锁;
  • 锁的获取、释放的方法;
  • 锁的失效机制,避免死锁的问题。

Redis里面提供了SETNX命令可以实现锁的排他性,当key不存在就返回1,存在就返回0,可以用expire命令设置锁的失效时间,从而避免死锁问题。

Redisson这个开源组件,就提供了分布式锁的封装实现,并且内置了一个Watch Dog机制来对key做续期

九、如何实现幂等

幂等是指一个方法被多次重复执行的时候产生的影响和第一次执行的影响相同。
幂等的核心思想就是保证这个接口的执行结果只影响一次,后续即便再次调用,也不能对数据产生影响。

  1. 使用数据库的唯一约束实现幂等,对于数据插入类的场景,比如创建订单,因为订单号肯定是唯一的,所以如果是多次调用就会触发数据库的唯一约束异常,从而避免一个请求创建多个订单的问题。
  2. 使用redis里面提供的setNX指令,比如对于MQ消费的场景,为了避免MQ重复消费导致数据多次被修改的问题,可以在接受到MQ的消息时,把这个消息通过setNX写入到redis里面,一旦这个消息被消费过,就不会再次消费。
  3. 使用状态机来实现幂等,所谓的状态机是指一条数据的完整运行状态的转换流程,比如订单状态,因为它的状态只会向前变更,所以多次修改同一条数据的时候,一旦状态发生变更,那么对这条数据修改造成的影响只会发生一次。

十、线程池如何知道一个线程的任务已经执行完成

在线程池内部,当我们把一个任务丢给线程池去执行,线程池会调度工作线程来执行这个任务的run方法,run方法正常结束,也就意味着任务完成了,所以线程池中的工作线程是通过同步调用任务的run()方法并且等待run方法返回后,再去统计任务的完成数量。
如果想在线程池外部去获得线程池内部任务的执行状态,有几种方法可以实现:

  1. 线程池提供了一个isTerminated()方法,可以判断线程池的运行状态,我们可以循环判断isTerminated()方法的返回结果来了解线程池的运行状态,一旦线程池的运行状态是Terminated,意味着线程池中的所有任务都执行完了,想要通过这个方法获取状态的前提是,程序中主动调用了线程池的shutdown()方法,在实际业务中,一般不会主动去关闭线程池,因此这个方法在实用性和灵活性方面都不是很好。
  2. 在线程池中,有一个submit()方法,它提供了一个Future的返回值,我们通过Future.get()方法来获得任务的执行结果,当线程池中的任务没执行完之前,future.get()方法会一直阻塞,直到任务执行结束,因此,只要future.get()方法正常返回,也就意味着传入到线程池中的任务已经执行完成了。
  3. 可以引入一个CountDownLatch计数器,它可以通过初始化指定一个计数器进行倒计时,其中有两个方法分别是await()阻塞线程,以及countDown()进行倒计时,一旦倒计时归零,所有被阻塞在await()方法的线程都会被释放,我们可以定义一个CountDownLatch对象并且计数器为1,接着在线程池代码块后面调用await()方法阻塞主线程,然后当传入到线程池中的任务执行完成后,调用countDown()方法表示任务执行结束。最后计数器归零,唤醒阻塞在await()方法的线程。

总结

每天都要有新的希望,如同重获新生一般!