并发和多线程(三)--并发容器J.U.C和lock简介

AQS:

  是AbstractQueuedSynchronizer的简称,JUC的核心

  底层是sync queue双向链表,还可能有condition queue单向链表,使用Node实现FIFO队列,可以用于构建同步队列或者其他同步装置的基础

框架

  利用了int类型表示状态,在AQS中有个state的成员变量,基于AQS的ReentrantLock,state表示获取锁的线程数,等于0,没有,1有,

大于1表示重入锁的数量。

protected final int getState() {    //获取当前同步状态
   return this.state;
}

protected final void setState(int var1) {    //设置当前同步状态
   this.state = var1;
}

//使用CAS设置当前状态,该方法能够保证状态设置的原子性
protected final boolean compareAndSetState(int var1, int var2) {
    return unsafe.compareAndSwapInt(this, stateOffset, var1, var2);
}

 基于模板方法,需要继承AQS,重写某些方法

可以实现排它锁和共享锁的模式(独占Reentrantlock、共享countdownlatch,同时实现一种)

1、实现思路:

  首先AQS中维护了一个queue来管理锁,线程会尝试获取锁,如果失败,就将当前线程以及等待状态等信息封装成一个node节点,加入到sync

queue的tail,head node的线程释放锁的时候,会唤醒队列中的后继线程,而后续节点在获取锁成功的时候把自己设置为首节点

就是因为这些设计,jdk有很多基于AQS的设计,一些常用的组件:

  countdownlatch、semaphore、CyclicBarrier、Reentrantlock、Condition、Futuretask等


2、同步组件:

2.1).CountDownLatch(闭锁):

  可以实现阻塞当前的线程,通过一个计数器进行初始化,这个计数器都是进行原子操作,只能同时有个线程操作这个计数器,调用CountDown

Latch的await()会处于阻塞状态,其他线程调用Countdown(),每次减一,直到计数器变成零

  这时候所有因为调用await()阻塞的线程才能继续往下执行,CountDownLatch只能执行一次,不能重置,想要使用重置的计数器,可以使用


2.2).CyclicBarrier

  await()需要等到countdown()将计数器减到0,才会执行后续的代码。await()可以有时间参数,选择等待多长时间过后就会执行await后续的

代码。

countDown()尽量卸载finally内部
countDownLatch.await();
countDownLatch.await(10, TimeUnit.MILLISECONDS);

使用场景:

2.3).Semaphore:信号量

  控制并发访问的个数,用于只能提供有限访问的资源

semaphore.acquire(3); // 获取多个许可
test(threadNum);
semaphore.release(3); // 释放多个许可
Semaphore semaphore = new Semaphore(2);允许线程数一定要大于等于acquire和release的个数
semaphore.tryAcquire()尝试获取许可,没有获取许可的线程都会丢弃
semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)在5000ms中尝试获取许可

 2.4).CyclicBarrier

  用于多线程计算数据,最后合并计算结果,例如Excel很多页流水,通过多线程计算每一页流水,最后计算总的

通过调用await()方法,线程进入等待状态,计数器进行加一操作,当值等于设置的初始值时,所有阻塞的线程继续执行

private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
    log.info("callback is running");
});

通过使用lambda,当计数器满足条件优先执行lambda表达式里面的代码

2.5).CyclicBarrier和CountDownLatch区别:

  1、CountDownLatch只能使用一次,而CyclicBarrier可以循环利用,使用reset进行重置

  2、CountDownLatch描述:1或N个线程需要等待其他线程完成某个操作,才能继续往下执行

  3、CyclicBarrier:多个线程之间相互等待,知道所有线程都满足某个条件才能继续执行后续操作,是各个线程直接相互等待的操作

countdown表现:    CountDownLatch表现:

1 is ready        1 is ready
2 is ready        2 is ready
3 is ready        3 is ready
1 continue        1 continue
2 continue        2 continue
3 continue        3 continue
4 is ready        4 is ready
4 continue        5 is ready
5 is ready        6 is ready
5 continue        4 continue
6 is ready        5 continue
6 continue        6 continue

LOCK

1、ReadWriteLock:

2、ReentrantReadWriteLock:

  支持多线程读,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

 

一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

读读共享、其他都是互斥

3、ReentrantLock:

  注意不要把lock的实例化做成局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同

的锁,这样lock就不能起作用了。

 

1、synchronized:

  可重入性,jvm实现,在之前和ReentrantLock性能差别很大,但是引入了偏向锁、轻量级锁,效率已经相差不大,只能使用非公平锁,

可以通过一些工具进行监控,jvm自动做加锁、解锁操作

2、ReentrantLock:

  可重入性,jdk实现,粒度更小,可以指定公平锁(先等待的线程先获得锁)或非公平锁,提供一个condition类,

可以实现分组唤醒需要唤醒的线程,而synchronized关键字要么唤醒一个线程,要么全部线程,可以通过lockInterruptibly()中断等待锁的线程

机制,一定要记得在finally释放锁

 

4、StampLock:

  对吞吐量有很大的改进,性能上有很大的提升,特别是适合读操作比较多的情况

ReentrantLock、ReentrantReadWriteLock、StampLock等lock都是对象层面的锁定

 

5、锁使用原则:

  1、当只有少量线程竞争的时候,可以使用synchronized,而且不会引发死锁

  2、线程竞争不少,线程增长能够预估,可以选择ReentrantLock

可重入锁:

  synchronized和ReentrantLock都是可重入锁

  锁的分配机制是基于线程的分配,而不是基于方法的分配,在method1中已经获取了对象锁,在方法内部调用method2不用重新获取锁。

可中断锁:

  synchronized就不是可中断锁,而Lock是可中断锁。

  lockInterruptibly()的用法时已经体现了Lock的可中断性。

公平锁:

  公平锁是指当一个锁被释放的时候,等待时间最长的线程会获取该锁,非公平锁可能导致某些线程永远不会获取到锁

  synchronized不是公平锁,ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

  ReentrantLock在实例化的时候参数true表示公平锁,false表示非公平锁,而且有很多判断锁状态的方法。

读写锁:

  多线程读操作不会发生冲突

condition:await()、signal()可以实现多路通知功能,但是通知部分线程要使用多个condition类,否则会全部唤醒

J.U.C组件拓展:

Callable与Runnable、Thread接口对比:

Future接口:

  可以得到线程任务方法的返回值

FutureTask类:

  实现了Runnable、Future,使用场景:线程A做一件事,线程B做别的事,在需要的时候可以的到线程A的返回值

Fork/Join(jdk1.7):

  就是把大任务拆分成若干小任务,放到双端队列,每个队列分配一个线程,先做完的线程帮助其他线程,一个从下面,一个从上面,并行执行

,最终汇总结果,但是某些情况下还是有线程竞争的情况

 

局限性:

  1、只能通过fork、join进行操作 2、不能有io操作 3、任务不能抛出检查异常

Queue:

  除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的

add(E e):

  将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常

remove():

  移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常

offer(E e):

  将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false

poll():

  移除并获取队首元素,若成功,则返回队首元素;否则返回null

peek():

  获取队首元素,若成功,则返回队首元素;否则返回null


注意点:
  1、对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法

以通过返回值

  2、判断操作成功与否,而使用add和remove方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。

  3、阻塞队列对于上面五个方法有做同步处理,而非阻塞队列没有同步

put(E e)
take()
offer(E e,long timeout, TimeUnit unit)
poll(long timeout, TimeUnit unit)

put方法用来向队尾存入元素,如果队列满,则等待;
take方法用来从队首取元素,如果队列为空,则等待;
offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;

 

1、BlockingQueue:

  主要用在生产者消费者场景,不需要关注什么时候阻塞和唤醒

2、ArrayBlockingQueue:

  有界的阻塞队列,就是容量是有限的,初始化指定容量大小,FIFO,内部是由数组实现

3、DelayQueue:

  必须实现Delay接口,它的元素要进行排序,应用场景:定时关闭连接、缓存对象,超时处理等

4、LinkedBlockingQueue:

  内部是链表,和ArrayBlockingQueue相似,FIFO

5、priorityBlockingQueue:

  允许插入null

6、SynchronousQueue:

  只能插入一个值,插入一个元素就会阻塞,也叫同步队列

线程池:

优点:

  1、减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

  2、可以根据系统的承受能力,调整线程池中工作线程的数据,防止因为消耗过多的内存导致服务器崩溃

 

ThreadPoolExecutor:

  注意corePoolSize、maximumPoolSize、workQueue等参数之间的关系,详情见9-1

参数:

1、corePoolSize

  核心池的大小。在创建了线程池之后,默认情况下,线程池中没有任何线程,而是等待有任务到来才创建线程去执行任务。默认情况下,在创建了线程池之后,

  线程池中的线程数为0,当有任务到来后就会创建一个线程去执行任务

2、maximumPoolSize

  池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量,当任务数量比corePoolSize大时,任务添加到workQueue,当workQueue满了,

将继续创建线程以处理任务,maximumPoolSize表示的就是wordQueue满了,线程池中最多可以创建的线程数量


3、keepAliveTime

  只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用。当线程数大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间

4、unit

  keepAliveTime时间单位

5、workQueue

  存储还没来得及执行的任务

6、threadFactory

  执行程序创建新线程时使用的工厂

7、handler

  由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序

解释:

  1、池中线程数小于corePoolSize,新任务都不排队而是直接添加新线程

  2、池中线程数大于等于corePoolSize,workQueue未满,首选将新任务加入workQueue而不是添加新线程

  3、池中线程数大于等于corePoolSize,workQueue已满,但是线程数小于maximumPoolSize,添加新的线程来处理被添加的任务

  4、池中线程数大于大于corePoolSize,workQueue已满,并且线程数大于等于maximumPoolSize,新任务被拒绝,使用handler处理被拒绝的任务

强烈建议程序员使用较为方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行线程自动回收)、

Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预

定义了设置。所以重点关注一下JDK推荐的Executors


并发最佳实践:

  1、使用本地变量

  2、使用不可变类

  3、最小化锁的作用范围:S=1/(1-a+a/n)

  4、宁可使用同步也不使用线程的wait和notify

  5、使用BlockingQueue实现生产-消费模式

  6、使用并发集合而不是加了锁的同步集合

  7、使用Semaphore创建有界的访问

  8、在使用synchronized时,宁可使用同步代码块,也不使用同步方法

  9、避免使用静态变量

 

转载于:https://www.cnblogs.com/huigelaile/p/10844784.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值