使用方式
Lock lock = new ReentrantLock();
lock.lock();
try {
// do something
}finally {
lock.unlock(); //在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放
}
tips:不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
以下是synchronize关键字不具备Lock接口的特点
Lock接口的API
队列同步器(AbstractQueuedSynchronizer)简称(AQS)其底层式基于一个双端链队实现
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
//重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
getState()://获取当前同步状态。
setState(int newState)://设置当前同步状态。
compareAndSetState(int expect,int update)://使用CAS设置当前状态,该方法能够保证状态设置的原子性。
以下是同步器可重写的方法
boolean tryAcquire(int arg); //独占式获取同步状态,
boolean tryRelease(int arg); //独占式释放同步状态
int tryAcquireShared(int arg); //共享式获取同步状态
boolean tryReleaseShared(int arg); //共享式释放同步状态
boolean isHeldExclusively(); //当前同步器是在否独占模式下被线程占用(其实翻译过来就是是否拥有排他机制)
实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7hqdIqH-1653877788242)(http://png.eot.ooo/i/2022/05/22/628a4154384f0.png)]
小练习:自定义一个同步组件 ——在同一时刻,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock。
思路
1、确定访问模式:TwinsLock能够在同一时刻支持多个线程的访问,这显然是共享式访问,需使用同步器提供的acquireShared(int args)方法等和Shared相关的方法,所以必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,以保证同步器的共享式同步状态的获取与释放方法得以执行。
2、自定义资源数:TwinsLock在同一时刻允许至多两个线程的同时访问,表明同步资源数为2,这样可以设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2。
//代码实现
public class TwinLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer{
Sync(int count){
if(count<0){
throw new IllegalArgumentException("参数必须大于零");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
while(true){
int current = getState();
int newCount = current-getState();
if(newCount<0 || compareAndSetState(current , newCount)) return newCount;
}
}
public boolean tryReleaseShared(int arg) {
while(true){
int current = getState();
int newCount = current+getState();
if(compareAndSetState(current , newCount)) return true;
}
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void unlock() {
sync.releaseShared(1);
}
//其余接口省略不计入
}
测试
public class TwinLockTest {
@Test
public void test() throws InterruptedException {
final Lock lock = new TwinLock();
class Worker extends Thread{
@SneakyThrows
public void run(){
while(true){
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} finally {
lock.unlock();
}
}
}
}
//启动10个线程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
//每隔一秒换行
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println();
}
}
}
测试结果可以发现每隔1S只有两个线程名字出现
内部接口
缓存代码实现
public class Cache {
static Map<String , Object> map = new HashMap<>();
static ReentrantReadWriteLock loc = new ReentrantReadWriteLock();
static Lock r = loc.readLock();
static Lock w = loc.writeLock();
//获取key对应的value
public static final Object get(String key){
//上读锁
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
public static final Object put(String key , Object value){
//上写锁
w.lock();
try {
return map.put(key,value);
} finally {
w.unlock();
}
}
public static final void clear(){
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
ockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPEfFnAT-1653877788252)(http://png.eot.ooo/i/2022/05/22/628a41520b9b4.png)]
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式
Condition接口与Object对象的差异
Condition接口示例代码
以下代码开启3个线程进行加数。没当一个计数器加到50的时候就进入休眠。当三个计数器达到休眠的时候,就进行唤醒所有的线程
public class Condi {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//进行自增
Runnable action = () -> {
int count = 0;
while (i < 1000) {
if(count<50) lock.lock();
System.out.println(Thread.currentThread().getName()+"---"+i++);
count++;
if(count==50){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"计数已经达到了50,进入短暂休眠");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
count=0;
lock.unlock();
}
}
lock.unlock();
}
};
//唤醒操作
Runnable pushlisher = () -> {
while(true){
lock.lock();
//当三个线程都进入休眠才进行唤醒
if (i%150==0) {
try {
System.out.println("所有线程都进入了休眠、开始唤起");
TimeUnit.SECONDS.sleep(3);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
};
//开启三个线程进行自增
Thread t1 = new Thread(action , "自增器一号");
Thread t2 = new Thread(action , "自增器二号");
Thread t3 = new Thread(action , "自增器三号");
Thread pusher = new Thread(pushlisher , "推送线程");
//设置守护线程
t1.setDaemon(true);
t2.setDaemon(true);
t3.setDaemon(true);
pusher.setDaemon(true);
t1.start();
t2.start();
t3.start();
//等上面的线程开启了
TimeUnit.SECONDS.sleep(2);
pusher.start();
TimeUnit.SECONDS.sleep(30);
System.out.println(Thread.activeCount());
}
}
1、线程不安全的HashMap :在多线程环境下使用HashMap进行put操作容易引起死循环
2、效率低下的HashTable :内部使用了synchronize重量所进行同步操作。
3、ConcurrenHashMap :分段锁进行提高效率
ConcurrenHashMap内部使用了分段锁将数据进行分段,每一段数据拥有其对应的锁、那么当多线程下访问容器的数据时,线程之间不会存在锁竞争的相关问题,同时当此段数据被占用的时候,其他数据部分也不会因为锁而导致无法进行读写访问
内部结构
ConcurrentHashMap其内部结构由Segment数组跟HashEntry构成、其Segment数组扮演者在容器中扮演者锁的角色。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁
ConcurentHashMap操作
1、get操作
Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素
get方法的高效之处在于整个get不需要加锁,其内部的变量都是基于volatile关键字实现
2、put操作
由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。
插入操作需要经历两个步骤,
第一步判断是否需要对Segment里的HashEntry数组进行扩容。
第二步定位添加元素的位置,然后将其放在HashEntry数组里。
(1)如何扩容
在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容,值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
(2)如何扩容
在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
3、size操作
统计ConcurrentHashMap的总大小不是简单的统计整个segment数组,在多线程环境下很可能在统计的时候有过写入操作而导致最后统计的大小与原来的大小不一样。
安全的做法:在进行size操作的时候会将 put、remove、clean这三个操作进行上锁。但是这种方法十分低效
ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化
在多线程环境下实现安全的队列有两种方式
1、通过阻塞队列来实现
2、是同非阻塞队列,通过CAS算法来实现(LinkedQueue实现)
出队列操作
public boolean offer(E e) {
//创建一个新要入队的节点
final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
for (Node<E> t = tail, p = t;;) {
//从后向前遍历
Node<E> q = p.next;
if (q == null) {
//当q为最后一个 进行CAS操作
if (NEXT.compareAndSet(p, null, newNode)) {
if (p != t) // 失败了也没关系,失败了表示其他线程成功进行了更新操作
TAIL.weakCompareAndSet(this, t, newNode);
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器
以下表是关于阻塞队列(BlockingQueue)的一些操作
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超市退出 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove(e) | poll() | take | poll(time , unit) |
检查 | element() | peek() | 不可用 | 不可用 |
·抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
·返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
·一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
·超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
tips: 如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。
java中的一些阻塞队列(实现BlockingQueue)
1、·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列
2、·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序
3、·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
4、·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
运用场景:(1)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了
(2)定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的
·5、SynchronousQueue:一个不存储元素的阻塞队列。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
·6、LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
7、·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
1、分割任务:首先需要有一个Fork类来把大任务分割成各个小任务、这里的分割根据自己的需求来确定
2、执行任务进行合并:被分割的任务放入双端队列当中、然后开启多个线程任务从双端队列队列中获取获取任务执行,执行完毕的结果最终都统一的放入队列当中,最后开启一个线程从队列中获取执行完毕的数据。
Fork/Join使用两个类来完成以上两件事情。
① ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
·RecursiveAction:用于没有返回结果的任务。
·RecursiveTask:用于有返回结果的任务。
② ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。
任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务
代码练习:实现1+2+3+4的简单小测试
使用Fork/Join框架首先要考虑到的是如何分割任务,如果希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2.
由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果
代码实例
public class ForkJ extends RecursiveTask<Integer> { private static final int THRESHOLD = 2; //阈值 private int start; private int end; public ForkJ(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; ForkJ leftTask = new ForkJ(start, middle); ForkJ rightTask = new ForkJ(middle + 1, end); //左任务开始执行 leftTask.fork(); //右任务开始执行 rightTask.fork(); //执行完成后得到结果 int left = leftTask.join(); int right = rightTask.join(); //合并任务 sum = left + right; } return sum; } public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinPool forkJoinPool = new ForkJoinPool(); //生成一个计算任务,负责计算 ForkJ forkJ = new ForkJ(1,4); //执行任务 Future<Integer> result = forkJoinPool.submit(forkJ); System.out.println(result.get()); } }
Fork方法实现原理
//当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步地执行这个任务,然后立即返回结果。
public final ForkJoinTask<V> fork() {
Thread t; ForkJoinWorkerThread w;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
(w = (ForkJoinWorkerThread)t).workQueue.push(this, w.pool);
else
ForkJoinPool.common.externalPush(this);
return this;
}
//pushTask方法把当前任务存放在ForkJoinTask数组队列里。然后再调用ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务。代码如下
final void externalPush(ForkJoinTask<?> task) {
WorkQueue q;
if ((q = submissionQueue()) == null)
throw new RejectedExecutionException();
else if (q.lockedPush(task))
signalWork(); //唤醒线程执行任务
}
Join方法实现原理
//Join方法的主要作用是阻塞当前线程并等待获取结果。ForkJoinTask的join方法的实现,代码如下
public final V join() {
int s;
if ((s = status) >= 0)
s = awaitDone(null, false, false, false, 0L);
if ((s & ABNORMAL) != 0)
reportException(s);
return getRawResult();
}
//这里s判断当前任务的状态来判断返回什么结果
/**
如果任务状态是已完成,则直接返回任务结果。
·如果任务状态是被取消,则直接抛出CancellationException。
·如果任务状态是抛出异常,则直接抛出对应的异常。
*/
并发包中提供很多的原子类这里就主要挑3类介绍同时重点介绍原子引用类
1、int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
2、boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
3、·int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
这里给出用法
public class AutomicReference {
static AtomicReference<user> atomicReferenceuser = new AtomicReference<user>();
public static void main(String[] args) {
user user = new user(10,"吉米");
atomicReferenceuser.set(user);
user updateUser = new user(20,"小红");
//CAS算法比较
atomicReferenceuser.compareAndSet(user,updateUser);
System.out.println(atomicReferenceuser.get().getName());
System.out.println(atomicReferenceuser.get().getAge());
}
}
class user{
int age;
String name;
public user(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
·AtomicLongFieldUpdater:原子更新长整型字段的更新器。
·AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题
代码示例
//第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。 //第二步,更新类的字段(属性)必须使用public volatile修饰符。 public class AutomicReference { static AtomicReference<user> atomicReferenceuser = new AtomicReference<user>(); static AtomicIntegerFieldUpdater<user> updater = AtomicIntegerFieldUpdater.newUpdater(user.class , "age"); public static void main(String[] args) { //设置年龄为10岁 user user = new user(10,"吉米"); //增长一岁 System.out.println(updater.getAndIncrement(user)); System.out.println(updater.get(user)); } } class user{ public volatile int age; //这里必须要用public volatile关键字修饰 private String name; public user(int age, String name) { this.age = age; this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
改工具类允许一个或者多个等待的线程
再次之前我们回顾一下线程中的join方法
public class JoinCountDownLatchTest {
static Lock lock = new ReentrantLock();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(() -> {
while(i<10){
lock.lock();
System.out.println("当前的i值为:" + i++);
lock.unlock();
}
}, "线程调度一号");
Thread two = new Thread(() -> {
for (int j = 0; j < 10; j++) {
System.out.println("在处理 - - -" + j);
try {
if(one.getState()!= Thread.State.TERMINATED) {
System.out.println(one.getName()+"开始加入线程");
}
//这里让一号线程加入到当前二号线程
one.join();
} catch (InterruptedException e) {
System.out.println("我被打断了");
}
}
}, "线程调度二号");
two.start();
//保证线程二的启动在线程一之前
TimeUnit.MILLISECONDS.sleep(10);
one.start();
}
}
/**
|-------|
|输出结果|
|-------|
在处理 - - -0
线程调度一号开始加入线程
当前的i值为:0
当前的i值为:1
当前的i值为:2
当前的i值为:3
当前的i值为:4
当前的i值为:5
当前的i值为:6
当前的i值为:7
当前的i值为:8
当前的i值为:9
在处理 - - -1
在处理 - - -2
在处理 - - -3
在处理 - - -4
在处理 - - -5
在处理 - - -6
在处理 - - -7
*/
通过一个例子能够明白Join方法的使用当A线程调用了B线程的Join方法之后A线程
Thread one = new Thread(() -> {
while (i < 10) {
lock.lock();
System.out.println("当前的i值为:" + i++);
lock.unlock();
}
}, "线程调度一号");
Thread two = new Thread(() -> {
for (int j = 0; j < 10; j++) {
System.out.println("在处理 - - -" + j);
try {
if (one.getState() != Thread.State.TERMINATED) {
System.out.println(one.getName() + "开始加入线程");
}
one.join();
} catch (InterruptedException e) {
System.out.println("我被打断了");
}
}
}, "线程调度二号");
Thread three = new Thread(() -> {
while (true) {
System.out.println(one.getName() + "当前处于" + one.getState());
System.out.println(two.getName() + "当前处于" + two.getState());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
three.setDaemon(true);
three.start();
two.start();
//保证线程二的启动在线程一之前
TimeUnit.MILLISECONDS.sleep(10);
one.start();
while (true);
/**
-------
|结果输出|
-------
在处理 - - -0
线程调度一号开始加入线程
线程调度一号当前处于NEW
线程调度二号当前处于WAITING
当前的i值为:0
当前的i值为:1
当前的i值为:2
当前的i值为:3
当前的i值为:4
当前的i值为:5
当前的i值为:6
当前的i值为:7
当前的i值为:8
当前的i值为:9
在处理 - - -1
在处理 - - -2
在处理 - - -3
在处理 - - -4
在处理 - - -5
在处理 - - -6
在处理 - - -7
在处理 - - -8
在处理 - - -9
*/
//可以看到B线程调用A线程join方法之后自身会陷入等待状态
//结束之后会调用notifyAll()
我们的CountDownLatch方法也能实现join方法,同时其功能比join()更多,代码示例
public class JoinCountDownLatchTest {
//创建CountDownLatch设置等待两个点
static CountDownLatch c = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(() -> {
System.out.println("当前工作线程工作" + Thread.currentThread().getName()); // 编号1
//计数-1
c.countDown();
});
Thread two = new Thread(()->{
System.out.println("当前工作线程工作" + Thread.currentThread().getName()); //编号5
//计数-1
c.countDown();
});
Thread three = new Thread(()->{
System.out.println("等待线程开始执行"); //编号2
try {
//进入等待
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待线程被唤醒"); //编号3
});
three.start();
//这里等待线程先执行
TimeUnit.SECONDS.sleep(1);
one.start();
two.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("Main线程结束"); //编号4
}
}
/**
只有当CountDownLatch(n)使用countDown使得n的计数达到0的时候线程才会唤醒所有等待的线程
如果这里改成3的话等待线程一直等待
*********
*执行结果*
*********
编号2 -> 编号1/编号5 -> 编号3 ->编号4
*/
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。(当进入await线程的数量等于构造函数的入参值得时候就当相当于打破屏障)
代码示例
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(() -> {
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(1);
}).start();
barrier.await();
System.out.println(2);
/**
这里如果构造填入得parties得值为3得时候则会一直进入堵塞状态
因为需要达到屏障的线程需要3个这里只调用了2此awiat方法
*/
此外CyclicBarrier
提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrier-Action),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景
public class JoinCountDownLatchTest {
static CountDownLatch c = new CountDownLatch(3);
static int i = 0;
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2,new WorkerThread());
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
Lock lock = new ReentrantLock();
Thread one = new Thread(() -> {
System.out.println("工作线程执行" + Thread.currentThread().getName());
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
while(i<10){
lock.lock();
i++;
lock.unlock();
}
});
Thread two = new Thread(() -> {
System.out.println("工作线程执行" + Thread.currentThread().getName());
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
while(i<10){
lock.lock();
i++;
lock.unlock();
}
});
one.start();
two.start();
//这里保证线程执行完毕
TimeUnit.SECONDS.sleep(3);
//应该是10
System.out.println(i);
}
static class WorkerThread implements Runnable{
@Override
public void run() {
//except zero actually it is zero
System.out.println("当前i的值应该为0,实际为:"+ i);
}
}
}
/**
----
|结果|
----
工作线程执行Thread-0
工作线程执行Thread-1
当前i的值应该为0,实际为:0
10
*/
CyclicBarrier应用场景
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景
例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水
//代码示例
public class CyclicBarrierTest implements Runnable {
/**
* 创建4个屏障,处理完之后执行当前Run方法
* */
private CyclicBarrier barrier = new CyclicBarrier(4 , this::run);
/**
* 假设只有4个sheet , 利用线程池开启4个线程 (后续会写Excutor框架)
* */
private Executor executor = Executors.newFixedThreadPool(4);
/**
* 保存每个sheet计算出来的银流结果
* */
private ConcurrentMap<String , Integer> map = new ConcurrentHashMap<>();
/**
* 放入四个容器中计算
* */
private void count(){
for (int i = 0; i < 4; i++) {
executor.execute(()->{
map.put(Thread.currentThread().getName() , 1);
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
public static void main(String[] args) throws InterruptedException {
CyclicBarrierTest test = new CyclicBarrierTest();
test.count();
}
/**
* 达到同步屏障的时候优先执行
* */
@Override
public void run() {
/**
* 将容器力的结果进行汇总
* */
int result = 0;
for (Map.Entry<String , Integer> entry : map.entrySet()){
System.out.println(entry.getKey());
result += entry.getValue();
}
/**
* 输出结果
* */
map.put("result",result);
System.out.println(map.get("result"));
}
}
/**
------------
输出结果为:4
------------
这里值得一提的是线程池没有关闭,因此当前线程会有6-5个线程还在运作
*/
以下是CountDownLatch于CyclicBarrier两者的区别
1、CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。
2、CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。
1、应用场景
Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。
public class SemaphoreTest { /** * 创建30个线程 * */ private static final int THREAD_COUNT = 30; private static ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); /** * 一次允许10个线程访问 * */ private static Semaphore s = new Semaphore(10); public static void main(String[] args) { for (int i = 0; i < THREAD_COUNT; i++) { executor.execute(()->{ try { s.acquire(); System.out.println("正在保存数据"); s.release(); } catch (InterruptedException e) { e.printStackTrace(); } }); } //关闭线程 executor.shutdown(); } } /** 这杨进行并行处理的时候一次最多只会有10个线程进行处理 */
2、其他方法
·intavailablePermits():返回此信号量中当前可用的许可证数。
·intgetQueueLength():返回正在等待获取许可证的线程数。
·booleanhasQueuedThreads():是否有线程正在等待获取许可证。
·void reducePermits(int reduction):减少reduction个许可证,是个protected方法。
·Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法。
1、作用以及简介
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
2、应用场景
1、遗传算法
Exchanger可以用于遗传算法 ,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
2、校对场景
Exchanger也可以用于校对工作 ,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。
3、代码示例①:A同学和B同学交换各自收藏的大片
public class Demo {
public static void main(String[] args) {
Exchanger<String> stringExchanger = new Exchanger<>();
Thread studentA = new Thread(() -> {
try {
String dataA = "A同学收藏多年的大片";
String dataB = stringExchanger.exchange(dataA);
System.out.println("A同学得到了" + dataB);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread studentB = new Thread(() -> {
try {
String dataB = "B同学收藏多年的大片";
String dataA = stringExchanger.exchange(dataB);
System.out.println("B同学得到了" + dataA);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
studentA.start();
studentB.start();
}
}
/*
* 输出结果:
* B同学得到了A同学收藏多年的大片
* A同学得到了B同学收藏多年的大片
*/
如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用**exchange(V x,longtimeout,TimeUnit unit)**设置最大等待时长。
代码示例②:A同学被放鸽子,交易失败
public class Demo {
public static void main(String[] args) {
Exchanger<String> stringExchanger = new Exchanger<>();
Thread studentA = new Thread(() -> {
String dataB = null;
try {
String dataA = "A同学收藏多年的大片";
//最多等待5秒
dataB = stringExchanger.exchange(dataA, 5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (TimeoutException ex){
System.out.println("等待超时-TimeoutException");
}
System.out.println("A同学得到了" + dataB);
});
studentA.start();
}
}
/*
* 输出结果:
* 等待超时-TimeoutException
* A同学得到了null
*/
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度 。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性 。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
当我们调用了excute方法的时候线程执行了什么操作呢?线程池内部又干了什么事情。
以下当一个新任务加入到线程池中线程池执行的流程
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecute执行流程
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数量小于基本的线程数,则创建线程同时执行当前任务
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程大于基本线程数、或者创建失败、则将其放入到工作队列当中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//如果线程池不处于运行中或任务无法放入队列、则新建一个线程
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
线程池中的线程执行任务分两种情况,如下。
1)在execute()方法中创建一个线程时,会让这个线程执行当前任务。
2)这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
参数介绍
1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
·ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
·LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法**Executors.newFixedThreadPool()**使用了这个队列。
·SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。·PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字,代码如下。
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
·AbortPolicy:直接抛出异常。
·CallerRunsPolicy:只用调用者所在线程来运行任务。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
excute()、summit()两个方法都可以向线程池提交任务
excute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
threadPool.execute(()->{
System.out.println("提交一次任务");
});
}
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
public class ThreadPoolTest {
static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
Future<?> future = threadPool.submit(() -> System.out.println("线程执行"));
try {
Object o = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}finally{
threadPool.shutdown();
}
}
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,
而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
想要合理的配置线程池就需要分析任务的特性,从以下几个角度来分析。
1、任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
2、任务的优先级:高、中和低。
3、任务的执行时间:长、中和短。
4、任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。
1)CPU密集型任务应配置尽可能小的线程,如配置Ncpu +1个线程的线程池。
2)由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu 。
3)混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
4)依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
5)建议使用有界队列 。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。
·taskCount:线程池需要执行的任务数量。
·completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
·largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
·getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
·getActiveCount:获取活动的线程数。
可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
·任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
·任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
·异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
以下是Executor框架的总体结构图
Executor框架使用示意图
/** proceed 3 step 1、 主线程首先要创建实现Runnable或者Callable接口的任务对象。 工具类Executors可以把一个Runnable对象封装为一个Callable对象( Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。 2、 然后把Runnable对象直接交给ExecutorService执行 (ExecutorService.execute(Runnablecommand)); 或者也可以把Runnable对象或Callable对象提交给ExecutorService执行 (Executor-Service.submit(Runnable task)或ExecutorService.submit(Callable
task)) 3、 如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象。 由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。 */
Executor框架主要成员由:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors组成
1)、ThreadPoolExecutor通常使用工厂类Executors来创建。
Executors可以创建3种类型的ThreadPoolExecutor:
1 、FixedThreadPool
/** 用来创建固定线程的API FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。 */ public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory); public static ExecutorService newFixedThreadPool(int nThreads); /** 运行详解:FixedThreadPool的 corePoolSize和maximumPoolSize都被设置为创建 FixedThreadPool时指定的参数nThreads。 ---------------------------- 返回结果:return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue
()); ---------------------------- 1、当运行线程数小于核心线程数量的时候,则创建新线程来执行任务 2、当前运行线程等于核心线程数的时候,将任务加入到LinkedBlockQueue 3、步骤1执行完成之后会循环反复从LinkedBlockQueue获取任务执行 ----- 3.1 当线程池中的线程数达到核心线程数量时候,任务将在任务队列中等待。因此线程数量永远不会超过CodePoolSize 3.2 由于1的作用的作用使用无界队列的时候maximumPoolSize将是一个无效参数 3.3 由于1和2,使用无界队列时keepAliveTime将是一个无效参数 3.4 因为LinkedBlockQueue是个无界队列,因此FiexedThreadPool不会执行拒绝策略 ---------------------------- 主线程 -execute->任务提交到阻塞队列 <---pool-corePool执行 */ 2、SingleThreadExecutor
/** SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。 */ public static ExecutorService newSingleThreadExecutor(); public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory); /** 运行详解 ------------------------------------ 返回结果:return new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue
())); ------------------------------------ SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1,同时其工作队列都是使用LinkedBlockQueue的无界队列来使用 ------------------------------------ 1、如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。 2、在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入Linked-BlockingQueue。 3、线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。 ------------------------------------ 基于以上的特性所以SingleThreadExecutor比较适合顺序的执行各个任务。 ---------------------------- 主线程 -execute->corePool(1)->阻塞队列 <-pool/take- corePoll */ 3、CachedThreadPool
/** CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。 */ public static ExecutorService newCachedThreadPool(); public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory); /** 参数设置:CachedThreadPool的corePoolSize被设置为0,即corePool为空; maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。 这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。 ------------------------- return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue
()); ------------------------- 详解 ---- CachedThreadPool使用SynchronousQueue作为线程池的工作队列,但基于该线程池的maximumPool是无界的,加入任务提交的速度高于线程池处理速度的时候,CachedThreadPool会不断创建线程,这样会导致CPU的资源被占满 ---------- 1、执行无界队列的offer工作如果maximumPool由空闲线程则队列执行pool匹配 execute方法就执行完成 2、当maximumPool为空,或者没有空闲线程时,步骤1会失败,此时线程池会在开启一个线程执行任务(execute) 3、在执行步骤2后线程不会立刻死亡,调用poll方法后会让线程等待60s,再次期间若由新的任务进入则会调用当前线程来执行,否则就会进入死亡 因此ChachedThreadPool在“待机”的时候不会过多的占用CPU资源 ------ 主线程--offer-->阻塞队列<-poll-空闲 ------ */
2)ScheduledThreadPoolExecutor通常使用工厂类Executors来创建。
Executors可以创建2种类型的ScheduledThreadPoolExecutor。
1、SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。
/** SingleThreadScheduledExecutor适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。 */ public static ScheduledExecutorService newSingleThreadScheduledExecutor(); public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory);
2、·ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。
/** ScheduledThreadPoolExecutor适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景 */ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize); public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory); /** 内部基于DelayQueue无界队列执行,因此ThreadPoolExecutor的maximumPoolSize在Scheduled-ThreadPoolExecutor中没有什么意义; --------------------- 1、当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWith-FixedDelay()方法时。 会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。 2、线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务 ----- 主线程 -创建scheduleAtFixedRate() -> add进入无界队列 <--执行---<--- 空闲队列 ----- */
3)Future接口
Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。
当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。
/**
将Runnable装换成Callable
*/
public static Callable<Object> callable(Runnable task);
public static <T> Callable<T> callable(Runnable task, T result);
/**
例如,如果提交的是对象Callable1,FutureTask.get()方法将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。
*/
4)Runnable接口和Callable接口
Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。
它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。
System.out.println(threadPool.submit(Executors.callable(() -> { System.out.println("Runnable到Callable的转换"); }, "返回结果")).get()); /** ---- Runnable到Callable的转换 返回结果 ---- */
FutureTask不仅实现了Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行 ,也可以又线程来直接执行。同时根据FutrueTask方法被执行的时机。FutureTask可以处于以下3中状态。
1)未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。
2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。
3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。
以下图是FutureTask的get以及cancel方法对于当前任务处于三个状态的影响
生产者与消费者模型
生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。
—转自简书—— 作者:浩宇天尚
PS:4核心8线程的!等于你有4个仓库,你要运输货物,8线程就是高速公路!8条高速公路送比你4条高速公路运的快吧!
有一个原则是:活跃线程数为 CPU(核)数时最佳。过少的活跃线程导致 CPU 无法被充分利用,过多的活跃线程导致过大的线程上下文切换开销。
线程应该是活跃的,处于 IO 的线程,休眠的线程等均不消耗 CPU。
计算密集型,顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
线程数= CPU核数+1
也可以设置成CPU核数2,这还是要看JDK的使用版本,以及CPU配置(服务器的CPU有超线程)。对于JDK1.8来说,里面增加了一个并行计算,计算密集型的较理想线程数 = CPU内核线程数2
对于IO密集型的应用,就很好理解了,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待的这段时间内,线程可以去做其它事,提高并发处理效率。
那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用:
线程数= CPU核心数/(1-阻塞系数)
这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调
tureTask处于未启动状态。
2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。
3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。
以下图是FutureTask的get以及cancel方法对于当前任务处于三个状态的影响
:[外链图片转存中…(img-zOQTGL0c-1653877788309)]
生产者与消费者模型
生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。
—转自简书—— 作者:浩宇天尚
PS:4核心8线程的!等于你有4个仓库,你要运输货物,8线程就是高速公路!8条高速公路送比你4条高速公路运的快吧!
有一个原则是:活跃线程数为 CPU(核)数时最佳。过少的活跃线程导致 CPU 无法被充分利用,过多的活跃线程导致过大的线程上下文切换开销。
线程应该是活跃的,处于 IO 的线程,休眠的线程等均不消耗 CPU。
计算密集型,顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
线程数= CPU核数+1
也可以设置成CPU核数2,这还是要看JDK的使用版本,以及CPU配置(服务器的CPU有超线程)。对于JDK1.8来说,里面增加了一个并行计算,计算密集型的较理想线程数 = CPU内核线程数2
对于IO密集型的应用,就很好理解了,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待的这段时间内,线程可以去做其它事,提高并发处理效率。
那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用:
线程数= CPU核心数/(1-阻塞系数)
这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调