本着重新学习(看到什么复习什么)的原则,这一篇讲的是JAVA的JUC。看了诸位大神的解释后详细的查了一些东西,记录下来,也感谢各位在网络上的分享!!!
blog推荐:
https://blog.youkuaiyun.com/weixin_44460333/article/details/86770169
https://www.cnblogs.com/aobing/p/12057282.html
1.什么是JUC:
在JAVA1.5之后,提供了java.util.concurrent包(也成为JUC)去处理多线程间并发编程问题,很多的工具包的出现让使用者能够在使用多线程编程时能够更有效,也更快捷。JUC内提供的工具类可以分为以下几类:
(1).locks:显式锁(互斥锁和读写锁)(AbstractQueuedSynchronizer,ReentantLock,ReentrantReadWriteLock)
(2).atomic:原子变量(AtomicBoolean,AtomicInteger,AtomicReference)
(3).executors:线程池(ThreadPoolExecutor,ForkJoinPool)
(4).collections:并发容器(ConcurrentHashMap,CopyOnWriteArrayList)
(5).tools:同步工具,信号量(Semaphore),闭锁(CountDownLatch),栅栏(CyclicBarrier)
2.什么是原子变量:
原子变量,即所有相关操作都应是原子操作。也就是说,对于原子变量的操作都是一步原子操作完成的。我们在进行多线程编程时会经常遇到多线程访问共享数据问题,而对于该问题的解决办法也有很多,如volatile关键字与synchronized关键字。volatile关键字是保证了在多线程访问共享数据时,能够在主存中数据可见。这是因为,原本每个线程会在使用变量时会在线程内存中创建一个该变量的拷贝,而在某线程修改数据过程中,这个修改操作是多线程间彼此不可见的,所以可能会发生主存中的数据仍未发生改变时被其他线程获取到,也就会发生线程安全的问题。synchronized关键字则能保证某数据在被多线程中某一线程修改时,不会再被其他线程获取到,也就是加锁。但是两者均有其不足。使用synchronized关键字去保证数据同步的方式实际上就是使用同步锁机制,需要对锁和线程的状态进行判断(如:该锁是否被获取,是否被释放,线程是否处于阻塞状态,是否再次处于就绪状态等),这样的判断操作效率很低。而使用volatile关键字虽然使用上降低了判断各种状态的内存消耗,但是会在某些场景下不能保持变量的原子性。(重排序)
volatile和synchronized的区别:
(1).volatile关键字只是保证了所有数据在读取变量时都是从主存中读取,synchronized关键字则是锁定该变量,即保证在单一时间只有一个线程访问该变量,其余线程处于阻塞状态。也就是说volatile关键字不具备互斥性。
(2).volatile关键字只能作用于变量之上,而synchronized关键字则能使用在变量,方法或类上
(3).volatile关键字只能保证变量的内存可见性,但是不能保证变量的原子性,而synchronized关键字能保证变量的内存可见性和原子性。针对原子性的理解又是最经典的i++操作,在内存中i++操作需要三步,即获取,更改,赋值,而这三步顺序执行才会保证i++操作的正常执行。
package com.day_7.excercise_1;
public class TryVolatile1 {
public static void main(String[] args) {
VolatileClass volatileClass = new VolatileClass();
new Thread(volatileClass).start();
while (true) {
// 2.
// synchronized (volatileClass) {
// if (volatileClass.isFlag()) {
// System.out.println("===============");
// break;
// }
// }
// 1.A
if (volatileClass.isFlag()) {
System.out.println("===============");
break;
}
}
}
}
class VolatileClass implements Runnable{
// 3.
private volatile boolean flag = false;
// 1.
// private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag:"+flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
由上可见,在使用场景1时,由于线程间的操作彼此不可见,所以虽然在VolatileClass创建的线程中对flag的值进行了修改,但是主线程中获取到的仍是主存中的flag的拷贝,所以无法正常退出循环。而在使用场景2或者场景3和场景1.A时,可以看到循环正常退出了,也就是说volatile关键字和synchronized关键字都能保证数据的内存可见性。
package com.day_7.excercise_1;
import java.util.concurrent.atomic.AtomicInteger;
public class TryAtomic1 {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
// 1.
// private volatile int i = 0;
// 2.
private AtomicInteger i = new AtomicInteger();
public int getI() {
// 1.
// return i++;
// 2.
return i.getAndIncrement();
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+getI());
}
}
由上可见,在使用场景1时,针对i++的操作结果并不能保证每一次获取到的数值都是不同的。所以volatile关键字不能保证原子变量的原子性,所以我们可以使用JUC中提供的Atomic类进行操作。在使用场景2时,可见操作每次获取到的值都是不同的,不重复也证明了其能够保证原子性。
Atomic相关:
在JUC中提供了诸如AtomicBoolean,AtomicInteger,AtomicReference等数据类型类,在其中封装的值都是使用volatile关键字修饰,也就保证了内存可见性,并且其中使用了CAS(Compare-And-Swap)算法保证了数据的原子性。
CAS:
CAS算法是在操作系统层面提供了对于并发共享数据访问的支持,其中包含三个操作数(内存值V,预估值A,更新值B),当且仅当V=A时才把B的值赋予V,否则将不进行操作。CAS比普通锁效率高的原因是因为使用CAS算法获取不成功时,不会阻塞线程,而是可以再次进行尝试更新。缺点在于会一直循环尝试。
例如:上图中线程1和线程2同时访问内存中的i变量,线程1获取(i = 0)即V=0,并且此时A=0,所以可以进行赋值操作,即将B赋值给V,而同一时间线程2访问获取i的值时,可能在获取V的值时V=0(i=0),但是在再次获取A的值时,已经被线程1进行了修改,那么此时A=1,由CAS算法可知,V!=A,所以不进行任何操作,也就不进行改变。下面是一个简单的cas实现:
package com.day_7.excercise_1;
public class TryCAS1 {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expectValue = cas.get();
boolean b = cas.compareAndSet(expectValue, (int)Math.random()*101);
System.out.println(b);
}
}).start();
}
}
}
class CompareAndSwap {
private int value;
// 获取内存值
public synchronized int get() {
return value;
}
// 比较
public synchronized int compareAndSwap(int expectValue, int newValue) {
int oldValue = value;
if (oldValue == expectValue) {
this.value = newValue;
}
return oldValue;
}
// 设置
public synchronized boolean compareAndSet(int expectValue, int newValue) {
return expectValue == compareAndSwap(expectValue, newValue);
}
}
3.什么是并发容器:
在JUC中也提供了很多保证线程安全的并发容器,用于处理单纯的容器类在处理多线程并发操作时的问题。如在使用本就不能保证线程安全的HashMap进行put操作时会出现的数据丢失或者数据覆盖的问题甚至有可能会导致死循环,使用了synchronized关键字的HashTable虽然保证了线程安全,但是其效率也正是由于使用synchronized关键字,故而形成了线程互斥,数据只能单一线程持有修改,而其他线程只能进行阻塞或者轮询,效率较低。所以在JUC中出现了ConcurrentHashMap类。
package com.day_7.excercise_1;
import java.util.HashMap;
import java.util.Map;
public class TryHashMapDieLoop {
public static void main(String[] args) {
HashMapThread thread0 = new HashMapThread();
HashMapThread thread1 = new HashMapThread();
HashMapThread thread2 = new HashMapThread();
HashMapThread thread3 = new HashMapThread();
HashMapThread thread4 = new HashMapThread();
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class HashMapThread extends Thread {
private static int intNum = 0;
private static Map<Integer, Integer> map = new HashMap<>();
@Override
public void run() {
while (intNum < 1000000) {
map.put(intNum, intNum);
System.out.println(map.size());
intNum++;
}
}
}
在JDK1.7时,ConcurrentHashMap使用的还是Segment分段锁和ReentrantLock锁机制,而在JDK1.8便修改为使用CAS和synchronized关键字来保证其并发安全性了。由此可见,现在synchronized关键字在使用时安全性更高一些,并且提高了查询遍历链表的效率。
此外在JUC中还提供了CopyOnWriteArrayList/CopyOnWriteArraySet(写入并复制),在添加操作多的场景下效率较低,因为每次添加时都会进行复制,开销很大。并发迭代操作多时可选择。
4.什么是闭锁:
CountDownLatch(闭锁):是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。每当一个线程完成其操作,计数器就会减1,当计数器归零时表示所有线程均已完成任务,而后在闭锁上等待的线程可以继续执行。其作用或者说是应用场景就在于:
(1).闭锁可以延迟线程的监督直到其到达终止状态,可以用来确保某些活动直到其他活动都完成才继续执行;
(2).确保某个计算在其需要的所有资源都被初始化之后才继续执行;
(3).确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
(4).等待直到某个操作所有参与者都准备就绪再继续执行。
package com.day_7.excercise_1;
import java.util.concurrent.CountDownLatch;
public class TryCountDownLatch {
public static void main(String[] args) {
// 相当于是维护一个阈值,在阈值递减直至为0时会继续进行接下来的操作
final CountDownLatch countDownLatch = new CountDownLatch(5);
CountDownLatchClass countDownLatchClass = new CountDownLatchClass(countDownLatch);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(countDownLatchClass).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为:"+(end-start));
}
}
class CountDownLatchClass implements Runnable{
private CountDownLatch countDownLatch;
public CountDownLatchClass(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 50000; i++) {
if(i%2==0) {
System.out.println(i);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 闭锁阈值递减1
countDownLatch.countDown();
}
}
}
}
如上所示,在使用主线程进行时间计算时,实质上是要等待所有其他线程完成后,才能得到本次程序运行的总时长,也就说明在其他线程做完之前,主线程应该处于等待状态。这便是闭锁的最佳使用场景,所以我们在使用CountDownLatch创建一个对象时,可以赋值其需要维护的计数器的阈值,并在阈值递减至0时,唤醒主线程。这个示例的场景可以拓展到很多场景,如我们在计算某一个具体操作之前,需要将其相关的所有操作全部执行成功,才能执行最终公式,或者我们在进行前后端交互时,我们需要对所有的数据库信息进行整合后,对这个结果集进行其他操作时,都可以并行的使用多线程,而后通过闭锁将某个关键线程等待,在适当时间唤醒。
5.什么是等待唤醒机制:
等待唤醒机制最常见的示例就是生产者/消费者问题,当生产者不断地生成数据,而消费者已经没有能力接受数据时,就会出现数据丢失的问题;当消费者不断的接收数据,而生产者已经不再发送数据时,就会出现重复数据的问题。并且当消费者不能获取到数据时,若不使用等待唤醒机制,则需要使用while(false)(false表示条件不满足)的无限循环进行轮询,同理,当生产者生产的数据不能加入到消费者的队列中,也就是说消费者队列是满足状态时,生产者也会使用类似的无限循环进行轮询。这些轮询的过程都是非常消耗CPU资源的,但是我们可以模拟一个场景,我们在食堂点餐后,等餐的时间中实际上不需要无限的询问,而是等待被叫号也好被告知已经做好了也好,这样我们在等待的过程中不是忙碌状态,也就可以有空闲的资源去做更多的事情,CPU也是同理。
与synchronized关键字相关联的等待唤醒机制方法是wait(),notify(),notifyAll()。则在使用这三个方法时必须在synchronized关键字的代码块或者方法中,并且由于可能存在虚假唤醒的问题,所以应该使用在while循环中。现假设我们有多个线程和一个生产者消费者队列,虚假唤醒的例子是这样的:
(1).在队列中存在一个任务1,线程1从队列中获取到这个任务,此时队列为空
(2).线程2通过判断队列中任务个数可知当前队列为空,不存在任务,所以线程2阻塞,等待队列非空
(3).队列中为空,所以生产者又生产了一个任务2,并且使用了notify进行唤醒
(4).处于等待状态的线程2可以获取任务,但是,与此同时,刚刚获取任务1的线程1可能已经做完任务1,此时也可以获取任务,则某一个线程获取任务并完成任务后,队列中又为空,此时等待状态的就变成了线程1和线程2,并且会在下一次获取到任务后,同样两者均等待。
在上述示例中,能够在(4)操作中被唤醒后获取到任务的就是正常执行,而另一个就是虚假唤醒。
package com.day_7.excercise_1;
public class TryComsumerProductor1 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Comsumer comsumer = new Comsumer(clerk);
new Thread(productor,"生产者A").start();
new Thread(comsumer,"消费者B").start();
new Thread(productor,"生产者C").start();
new Thread(comsumer,"消费者D").start();
}
}
class Clerk{
private int product = 0;
public synchronized void get() {
// if (product>=10) {
// A
// if (product>=1) {
// B
while(product>=10) {
System.out.println("产品已满");
try {
// 当不能正确接收产品时,就等待
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// else {
System.out.println(Thread.currentThread().getName()+":"+(++product));
// 当已经有产品被成功生产,就通知所有线程
this.notifyAll();
// }
}
public synchronized void sale() {
// if (product<=0) {
// B
while(product<=0) {
System.out.println("产品已缺货");
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// else {
System.out.println(Thread.currentThread().getName()+":"+(--product));
this.notifyAll();
// }
}
}
class Productor implements Runnable{
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// A
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
clerk.get();
}
}
}
class Comsumer implements Runnable{
private Clerk clerk;
public Comsumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
如上可以看到,定义了Comsumer和Productor,并且在使用if判断时,会出现虚假唤醒的情况。而在使用while循环时则没有这种问题出现。在JUC则使用Lock和Condition来实现等待唤醒机制。对应的方法为wait(),signal(),signalAll()。
package com.day_7.excercise_1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryComsumerProductor2 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Comsumer comsumer = new Comsumer(clerk);
new Thread(productor, "生产者A").start();
new Thread(comsumer, "消费者B").start();
new Thread(productor, "生产者C").start();
new Thread(comsumer, "消费者D").start();
}
}
class Clerk1 {
private int product = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void get() {
lock.lock();
try {
while (product >= 10) {
System.out.println("产品已满");
try {
// 当不能正确接收产品时,就等待
condition.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (++product));
// 当已经有产品被成功生产,就通知所有线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void sale() {
lock.lock();
try {
while (product <= 0) {
System.out.println("产品已缺货");
try {
condition.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (--product));
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class Productor1 implements Runnable {
private Clerk1 clerk1;
public Productor1(Clerk1 clerk1) {
this.clerk1 = clerk1;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// A
// try {
// Thread.sleep(200);
// } catch (Exception e) {
// e.printStackTrace();
// }
clerk1.get();
}
}
}
class Comsumer1 implements Runnable {
private Clerk1 clerk1;
public Comsumer1(Clerk1 clerk1) {
this.clerk1 = clerk1;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk1.sale();
}
}
}
6.什么是Condition:
Condition接口提供了类似于Object的监视器方法,可以与Lock配合使用(或者可以说condition对象是依赖于Lock对象的,创建方法就是Lock对象.newCondition()),实现等待唤醒机制。在AQS中有内部类ConditionObject实现了Condition接口,每个Condition对象中维护了一个FIFO的等待队列。
7.什么是线程池:
多线程执行能够最大限度的提高程序运行速度,但是与数据库也拥有连接池一样,对于创建一个连接或者线程,再经过使用后将其销毁的过程也是非常耗费CPU资源的。所以我们可以使用线程池来避免某些问题的出现(为什么有线程还要有线程池?):
(1).避免了线程的创建和销毁带来的性能消耗:这就避免了重复的创建和销毁的过程,也就使得CPU资源的消耗降低了。因为在线程池中的线程可以重复使用。
(2).避免大量线程因为互相抢占系统资源而导致的阻塞现象:这就避免了由于不断创建线程,但是可能获取资源的线程只有一个的可能性下,会导致的多线程阻塞状态。
(3).能够对线程进行简单的管理并提供定时执行,间隔执行等功能
此外,要明确的是,因为多线程中每一个线程同样都是在使用CPU资源,所以线程并不是越多越好。效率上也有着区别。
(1).不使用线程池
package com.day_7.excercise_1;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class TryThreadNoPool {
public static void main(String[] args) throws Exception {
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<Integer>();
final Random random = new Random();
for(int i = 0;i<10000;i++) {
Thread thread = new Thread() {
public void run() {
list.add(random.nextInt());
}
};
thread.start();
// 主线程等待一起完成
thread.join();
}
System.out.println("总耗时:"+(System.currentTimeMillis()-start));
System.out.println("总大小:"+list.size());
}
}
(2).使用线程池
package com.day_7.excercise_1;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TryThreadWithPool {
public static void main(String[] args) throws Exception {
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
final Random random = new Random();
for(int i = 0;i<10000;i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("总耗时:"+(System.currentTimeMillis()-start));
System.out.println("总大小:"+list.size());
}
}
在线程池的创建上,Executors工具类中提供了很多的方法,如下:
方法名 | 功能 | 备注 | 适用场景 |
---|---|---|---|
newFixedThreadPool | 固定大小的线程池 | 在线程池中一旦有线程处理完手中的任务就会处理新的任务 | 适用于负载较重,需要满足资源管理限制线程数量的场景 |
newSingleThreadExecutor | 只有一个线程的线程池 | 只会创建一个线程,会按照任务提交入队列顺序执行 | 适用于需要保证任务顺序执行的场景 |
newCachedThreadPool | 不限线程数上限的线程池 | 首先会创建足够多的线程,线程在程序运行过程中可以循环使用,仅在可以循环使用线程执行任务时,才不创建新的线程 | 适用于负载较轻,需要执行很多短期异步任务的场景 |
...... | ...... |
package com.day_7.excercise_1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TryExecutor1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(()->System.out.println("线程池"));
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程池");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程池");
}
});
executorService.shutdown();
}
}
我们来看一下ThreadPoolExecutor的继承和实现流程,还有调用的部分过程。
ThreadPoolExecutor—>AbstractExecutorService(接口实现类)—>ExecutorService(接口提交)—>Executor(执行接口)
public interface Executor {
void execute(Runnable command);
}
public interface ExecutorService extends Executor {
......
}
public abstract class AbstractExecutorService implements ExecutorService {
......
// 方法来自ExecutorService接口
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 执行来自Executor接口
execute(ftask);
return ftask;
}
......
}
public class ThreadPoolExecutor extends AbstractExecutorService {
......
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);
}
......
}
在ThreadPoolExecutor的构造方法中提供了很多参数来配置线程池:
(1).corePoolSize:(the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set)线程池内被一直保持存活状态的的核心线程数量。这些线程即使是闲置的,也会存活在线程池汇总,除非设置allowCoreThreadTimeOut参数。
(2).maximumPoolSize:(maximumPoolSize the maximum number of threads to allow in the pool)线程池内最大线程数量。
(3).keepAliveTime:(when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.)非核心线程的最大闲置时长,超过这个时长便会被回收。
(4).unit:(the time unit for the {@code keepAliveTime} argument)用于指定keepAliveTime参数的时间单位,通过TimeUnit枚举类来确定该单位。
(5).workQueue:(the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.)用于存储任务的任务队列。只用来接收创建的Runnable线程对象,因为泛型类型已经被指定(BlockingQueue<Runnable> workQueue)。
(6).threadFactory:(the factory to use when the executor creates a new thread)线为线程池创建新线程的线程工厂,该接口中只有一个创建线程的方法(Thread newThread(Runnable r))。
(7).RejectedExecutionHandler:(the handler to use when execution is blocked because the thread bounds and queue capacities are reached)当该ThreadPoolExecutor已经被关闭或者线程池内的任务队列已经饱和时的通知策略。
addWorker方法意在检查能否在当前池内状态和绑定核心线程和最大线程的数量情况下添加新的线程进行工作。如果可能,则创建并启动一个新的工作线程,并且会在线程创建失败时回滚。方法参数firstTask为传入的任务,core为是否使用核心线程池作为绑定或是使用最大线程数量作为绑定,方法返回布尔值。
所以对于拥有以上7个参数的场景可以简述为:
1.首先使用核心线程内的线程;
2.在核心线程均在工作时,将任务存在等待队列中;
3.当等待队列未满,且核心线程可以满足任务的情况下,不创建新的线程;
4.当等待队列已满,且核心线程不可以满足任务的情况下,创建新的线程进行任务,直至达到最大线程数量;
5.当等待队列已满,且核心线程不可以满足任务的情况下,并且已经达到最大线程数量时,执行拒绝策略;
1.构造方法:初始化
// 核心线程池内线程数量,最大线程池内数量,保持时长(创建到销毁的时间),时间单位,工作队列,线程工厂类,拒绝策略
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
2.submit方法:
// java.util.concurrent.ThreadPoolExecutor.execute(Runnable)
public void execute(Runnable command) {
// 判断传入的任务是否为空
if (command == null)
throw new NullPointerException();
// 原子操作,获取状态
int c = ctl.get();
// 判断当前运行的线程数量<corePoolSize 创建新的线程执行新添加的任务,并返回
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// private final BlockingQueue<Runnable> workQueue;
// 进入该判断分支说明当前运行线程数量>=corePoolSize ,判断当前线程池状态是否正在运行和能否将该任务加入任务队列等待执行
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次判断线程池状态,若已经不是运行状态,则应该拒绝新任务,并从队列中删除任务(double check)
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程数量未达到线程最大规定线程数量,则会启动一个非核心线程执行任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果仍然无法创建线程持有任务,就拒绝这个任务
else if (!addWorker(command, false))
reject(command);
}
3.addWorker方法:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 判断能否进行CAS操作,使得worker数量+1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// private final HashSet<Worker> workers = new HashSet<Worker>();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
我们常用的有两种基础线程池:ThreadPoolExecutor和ScheduledThreadPoolExecutor,前者是线程池的核心实现类,用来执行被提交的任务,并且由于提供了很多方便调用的线程池创建方法,所以真正创建线程池的构造方法被固定并隐藏,虽说确实可以直接通过构造方法进行线程池的创建,却不具备后者的延迟执行等功能;后者时候一个支持线程调度的线程池实现类,能够在给定的延迟后运行命令,或者定时执行。
继承了AbstractExecutorService的ForkJoinPool是一种特殊的线程池。它支持将一个任务拆分成多个小任务并行计算,而后将多个小任务的结果合成为总的计算结果。通过调用submit或invoke方法执行额任务,方法接收泛型的ForkJoinTask。ForkJoinTask拥有两个抽象子类,RecusiveAction和RecusiveTask。前者代表没有返回值的任务,而后者代表有返回值的任务。ForkJoinTask使用递归来实现子任务的方法调用,但是要注意的是,任务拆分机制需要人为进行干预,也就是说要想得到更优质的执行效果需要多次尝试。
package com.day_7.excercise_1;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
import org.junit.Test;
public class TryForkJoinPool {
public static void main(String[] args) {
Instant start = Instant.now();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinTask = new ForkJoinDemo(0L, 10000000L);
Long sum = pool.invoke(forkJoinTask);
System.out.println(sum);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//783
}
@Test
public void test1(){
Instant start = Instant.now();
Long sum = 0L;
for (Long i = 0L; i <= 1000000L; i++) {
sum+=i;
}
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//112
}
//JAVA8新特性
@Test
public void test2(){
Instant start = Instant.now();
Long sum = LongStream.rangeClosed(0L, 10000000L).parallel().reduce(0L, Long::sum);
System.out.println(sum);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//287
}
}
//RecursiveTask有返回值
//RecursiveAction没有返回值
class ForkJoinDemo extends RecursiveTask<Long>{
/**
* THURSHOLD设置拆分到什么情况下才不进行拆分,即临界值
*/
private static final long serialVersionUID = 1L;
private Long start;
private Long end;
// 临界值
private static final long THURSHOLD = 1000L;
public ForkJoinDemo(Long start, Long end) {
super();
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end-start;
if(length<=THURSHOLD) {
long sum = 0L;
for (long i = start; i <= end ; i++) {
sum += i;
}
return sum;
}else {
long middle = (start+end)/2;
ForkJoinDemo left = new ForkJoinDemo(start, middle);
// 进行拆分,同时压入线程队列
left.fork();
ForkJoinDemo right = new ForkJoinDemo(middle+1, end);
right.fork();
return left.join()+right.join();
}
}
}
8.什么是Lock:
JAVA并发编程关于锁的实现方式有两种:基于synchronized关键字的锁实现(JVM内置锁,隐式锁:由JVM自动加锁解锁),基于Lock接口的锁实现(显式锁,需要手动加锁加锁)。基于Lock接口的实现类包括上一篇中的ReentrantLock可重入锁,ReadWriteLock读写锁和StampedLock戳锁等。
读写锁,即多线程都在做同样的写操作(写写/读写)时需要互斥,而读读操作不需要互斥。同时维护两个锁,读锁(多个读线程并发执行)和写锁(写锁是单线程独占的,同步锁)。如下,我们可以看到,在使用get方法进行读操作时,很快就能完成,但是在使用set方法进行写操作时,则是每隔1s执行一次。
package com.day_7.excercise_1;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//
public class TryReadWriteLock {
public static void main(String[] args) {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
// 写
// for (int i = 0; i < 10; i++) {
// new Thread(new Runnable() {
// @Override
// public void run() {
// readWriteLockDemo.set((int)(Math.random()*101));
// }
// },"Write").start();
// }
// 读
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriteLockDemo.get();
}
},"Read").start();
}
}
}
class ReadWriteLockDemo{
private int number = 1;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void get() {
lock.readLock().lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+number);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public void set(int number) {
lock.writeLock().lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+number);
this.number = number;
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
学习了很多关于JUC的知识,明确了很多内容,非常感谢特别香的视频课程和文首的两篇文章,很详尽也很易懂,需要多看看加深印象。万分感谢大家在网络上的资源贡献!