JUC
阻塞队列
阻塞队列(BlockingQueue
)通常用于一个线程生产对象,而另一个线程消费这些对象的场景。该队列具有缓冲、消峰限流、解耦生产者与消费者等作用。阻塞队列底层是通过锁机制实现的。队列的4个插入方法:
- add:当队列已满,会抛出异常;
- offer:插入成功返回true,失败返回false;
- offer(obj, 3000, TImeUnit.xxx):阻塞指定时间后仍插入失败则返回false;
- put:当队列已满,则阻塞;(常用)
队列的删除方法:
- remove:当队列为空时会抛异常;
- poll:当队列为空时会返回null;
- poll(3000, TimeUnit.xxx):阻塞指定时间后仍然为空则返回null;
- take:当队列为空时,会产生阻塞;(常用)
队列的五个实现类的特点:
- ArrayBlockingQueue:在创建时指定容量,;
- LinkedBlockingQueue:默认大小是Integer,MaxValue,;
- PriorityBlockingQueue:优先级队列,可以排序,使用该队列的对象需要实现Comparable接口;
- SyschronousQueue:同步队列,容量只能为1;
- BlockingDeque:阻塞双端队列,有两套插入和移出方法;
并发映射
常见映射有:
- HashMap:性能高,但是线程不安全
- HashTable:性能很低,线程安全,所有方法都使用同步代码块
锁的代价:需要进行上下文切换、和调度延时,等待锁的线程会被挂起直至锁释放,对性能影响很大。
并发哈希映射(ConcurrentHashMap
),底层是基于数组+链表,容量默认是16,加载因子默认0.75,扩容默认增加一倍,ConcurrentHashMap在jdk1.8之前为:引入了分段锁机制,底层分了16端,每一段都可以看做是一个HashTable,并发性能更高。1.8之后:使用了CAS无锁算法(Compare and swap):是一种乐观锁技术,没有锁的开销,但是可能会导致失败重试的次数很多,并且将链表替换为红黑树。
CAS:即我认为V的值应该是A,如果是,则将V的值更新为B,否则,不修改,并告诉V的实际值,然后重试;
ConcurrentNavigableMap接口:其实现类是ConcurrentSkipListMap,使用了跳表实现
闭锁
闭锁(CountDownLatch
)是一个并发构造,允许一个或者多个线程等待一系列操作完成;使用方法是:给定一个数量初始化,每调用一次countDown方法数量减一,等待线程则通过调用await方法等待这一数量到达零才继续执行。
示例代码:(主线程会等到两个线程的任务都执行完了才会执行)
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch down = new CountDownLatch(2);
new Thread(new T1("t1", down)).start();
new Thread(new T1("t2", down)).start();
down.await();
System.out.println("主线程执行了!");
}
static class T1 implements Runnable{
private String name;
private CountDownLatch down;
T1(String name, CountDownLatch down){
this.name = name;
this.down = down;
}
@Override
public void run() {
System.out.println(name + "线程执行完成!");
down.countDown();
}
}
}
栅栏
栅栏(CyclicBarrier
)是一种同步机制,可以试想线程的同步协调,能够对处理一些算法的线程实现同步,也就是说所有线程都需要到达每一点才能继续执行;使用方法:先给定一个数量初始化,每个线程完成一些预处理操作后则执行await方法,每调一次await方法数量减一,当减为零时,所有阻塞方法继续执行。
示例代码:(尽管三个线程预处理时长不同,但最终会等待其他线程执行完成了才继续)
public class CyclicBarrierTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyc = new CyclicBarrier(3);
new Thread(new T1("t1", 3, cyc)).start();
new Thread(new T1("t2", 1, cyc)).start();
System.out.println("主线程预处理完成!");
cyc.await();
System.out.println("主线程执行完成!");
}
static class T1 implements Runnable {
private CyclicBarrier cyc;
private int sec;
private String name;
T1(String name, int sec, CyclicBarrier cyc) {
this.name = name;
this.sec = sec;
this.cyc = cyc;
}
@Override
public void run() {
try {
Thread.sleep(sec * 1000);
System.out.println(name + "线程预处理完成!");
cyc.await();
System.out.println(name + "线程执行完成!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
交换机
交换机(Exchanger
)可以交换两个线程的数据,使用方法是直接调用exchange方法,参数为要传递的参数,返回值是对方线程传递的值;示例代码如下:(线程1打印了线程2的消息,线程2打印了线程1的消息)
public class ExchangerTest {
public static void main(String[] args) {
Exchanger<String> ex = new Exchanger<>();
new Thread(new T1("t1", "消息一", ex)).start();
new Thread(new T1("t2", "消息二", ex)).start();
}
static class T1 implements Runnable{
private String name;
private String msg;
private Exchanger<String> ex;
T1(String name, String msg, Exchanger<String> ex){
this.name = name;
this.msg = msg;
this.ex = ex;
}
@Override
public void run() {
try {
String str = ex.exchange(msg);
System.out.println(name + "线程收到消息:" + str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程池
也叫执行器服务ExecutorService
(实现类为ThreadPoolExecutor
)。线程池的初始化参数有核心线程数、最大线程数、存活时间、时间单位、等待队列大小、拒绝服务助手,当提交线程到线程池时,会优先启用核心线程,核心满了则会放到等待队列,等待队列也满了,则启用临时线程,临时线程也达到最大后,则会拒绝服务;除了直接创建线程池,Executors
中封装了4中不同参数的线程池如下:
- 缓存型线程池(
newCachedThreadPool
):即大池子小队列,该线程池可以很好的响应客户端请求,适合于高并发的短请求,但如果是长请求,可能会导致线程一致创建而不销毁,进而出现堆溢出,具体特点为:- 没有核心线程;
- 最大线程数为Integer.MAX_VALUE;
- 存活时间为1分钟;
- 队列是同步队列。
- 固定数量线程池(
newFixedThreadPool
):即小池子大队列,该线程池可以消峰限流,但是可能不能及时响应请求,具体特点为:- 核心线程数等于最大线程数,即没有临时线程,
- 队列是链表阻塞队列,可以认为无界;
- 调度性线程池(
newScheduledThreadPool
):- 固定核心线程数,
- 最大线程数Integer.MAX_VALUE,
- 使用无界阻塞延迟队列(DelayedWorkQueue类),并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列,
- 可以延时(
schedule
方法)或者周期性(scheduleAtFixedRate
方法/scheduleWithFixedDelay
方法)执行线程。
- 单线程线程池(newScheduledThreadPool):单个核心线程,最大线程1,链表阻塞队列。
Callable
是jdk1.5之后提供的新的线程机制,使用call方法执行任务,具有如下特点:
- 可以自定义返回值类型;
- 返回值可以接收;
- 可以抛异常;
- 只能通过线程池启动。其返回值通过
Future
的get方法(阻塞方法)接收。
示例代码如下:
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
Future<String> future = service.submit(new T1());
System.out.println(future.get());
service.shutdown();
}
static class T1 implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("线程执行了!");
return "SUCCESS";
}
}
}
分叉合并
- 是jdk1.7以后出现的;
- 将大任务进行拆分成多个小任务分配给不同的线程来执行;
- 将拆分的任务的执行结果进行合并;
- 这种方式可以提高CPU利用率,在数据量大时可以提高效率;
- 分叉合并在分配任务时,会自动平衡任务,若核上原来的任务多则会少分,否则多分;
- 分叉合并为了避免慢任务导致的效率降低,采取了work-stealing(工作窃取)策略;即当有核心完成任务后,会随机从其他核心的任务队列尾端窃取任务来执行。
- 使用方法是让任务类继承
RecursiveTask
抽象类,该类的泛型就是结果的类型,如果不需要结果则继承RecursiveAction
抽象类;
//分叉合并示例代码,切分累加运算
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinTest {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
Sum s = new Sum(0L, 10000000000L, true);
pool.execute(s);
Long result = s.join();
System.out.println(result);
}
}
class Sum extends RecursiveTask<Long> {
private Long start;
private Long end;
private boolean task; //任务拆分条件
Sum(Long start, Long end, boolean split) {
this.start = start;
this.end = end;
this.task = split;
}
@Override
protected Long compute() {
if (task) {
Long mid = (end + start) / 2;
Long mid1 = (mid + start) / 2;
Long mid2 = (end + mid) / 2;
Sum s1 = new Sum(start, mid1, false);
Sum s2 = new Sum(mid1, mid, false);
Sum s3 = new Sum(mid, mid2, false);
Sum s4 = new Sum(mid2, end, false);
s1.fork();
s2.fork();
s3.fork();
s4.fork();
return s1.join() + s2.join() + s3.join() + s4.join();
}else{
Long result = 0L;
for (Long i = start; i < end; i++) {
result += i;
}
return result;
}
}
}
锁(Lock)
功能类似同步代码块(synchronized),但是比同步代码块功能更强大;它支持两种锁机制(公平锁/非公平锁,默认为非公平)。同步代码块只有非公平锁。锁还支持读写锁。
例题
实现ABCD四个线程轮流输出10次
方式一
使用ABCD四把锁分别对应每个线程,每个线程只有获取到自己的锁和下一个线程的锁才开始执行,执行逻辑是先执行业务代码(即输出一次),接着唤醒下一个线程,然后用自己的锁阻塞自己;但这个逻辑需要保证第一次执行的顺序正确,故又添加了闭锁,让第一次执行时,D需要等待C,C需要等待B,B需要等待A;还有在最后一次输出后线程就不需要阻塞了,否则会因为没有唤醒它的线程使得程序不能正常结束,具体代码如下:
import java.util.concurrent.CountDownLatch;
//可以完成每次都顺序输出
public class Main {
public static void main(String[] args) {
CountDownLatch a = new CountDownLatch(1);
CountDownLatch b = new CountDownLatch(1);
CountDownLatch c = new CountDownLatch(1);
CountDownLatch d = new CountDownLatch(1);
Object aa = new Object();
Object bb = new Object();
Object cc = new Object();
Object dd = new Object();
new Thread(new A(null, a, "A", aa, bb)).start();
new Thread(new A(a, b, "B", bb, cc)).start();
new Thread(new A(b, c, "C", cc, dd)).start();
new Thread(new A(c, null, "D", dd, aa)).start();
}
}
class A implements Runnable {
private CountDownLatch self;
private CountDownLatch pre;
private String name;
private final Object curr;
private final Object next;
public A(CountDownLatch pre, CountDownLatch self, String name, Object curr, Object next) {
this.pre = pre;
this.self = self;
this.name = name;
this.curr = curr;
this.next = next;
}
@Override
public void run() {
try {
if (pre != null) {
pre.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i < 11; i++) {
synchronized (curr) {
synchronized (next) {
System.out.println(this.name + "-" + i);
next.notify();
}
try {
if(i == 1)
if(self != null)
self.countDown();
if(i != 10)
curr.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
方式二
先定义两个全局变量已经输出的次数(0)和总共需要输出的次数(40),接着给每个线程设置0,1,2,3的编号,然后所有线程并发抢锁,抢到锁后先判断已经输出的次数(0)对线程数(4)取余的值是否为当前线程的编号,若是则输出一次并将已经输出的次数加一,然后唤醒所有线程,否则当前线程阻塞并释放锁,由剩下的线程抢锁,直到已经输出的次数等于总共需要输出的次数;具体代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//可以保证每一轮都顺序输出
public class Main1 {
static int total = 10 * 4;
static int curr = 0;
static String[] name = {"A", "B", "C", "D"};
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(new T1(0, lock, condition)).start();
new Thread(new T1(1, lock, condition)).start();
new Thread(new T1(2, lock, condition)).start();
new Thread(new T1(3, lock, condition)).start();
}
}
class T1 implements Runnable {
private Lock lock;
private Condition condition;
private int index;
public T1(int index, Lock lock, Condition condition) {
this.index = index;
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
while (Main1.curr < Main1.total) {
lock.lock();
if (Main1.curr % 4 == index) {
System.out.print(Main1.name[index]);
if (Main1.curr % 4 == 3) {
System.out.println("-" + (Main1.curr / 4));
}
Main1.curr++;
condition.signalAll();
} else {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
}
}
方式三
所有线程并发抢锁,抢到锁的线程则执行业务代码(输出一次),接着将本轮的业务代码执行次数(输出次数)加一,然后判断本轮执行的总次数是否等于线程数(4),若是则代表本轮所有线程都执行一次了,故重置本轮执行次数为零,唤醒所有线程;否则代表还有线程本轮没有执行,则阻塞等待;具体代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//感觉这种方式效率较高,但是只能保证每一轮所有线程都执行一遍,不能保证每一轮都有序;
public class Main2 {
static int total = 10;
static String[] name = {"A", "B", "C", "D"};
static int state = 0;
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(new T2(0, lock, condition)).start();
new Thread(new T2(1, lock, condition)).start();
new Thread(new T2(2, lock, condition)).start();
new Thread(new T2(3, lock, condition)).start();
}
}
class T2 implements Runnable {
private Lock lock;
private Condition condition;
private int index;
public T2(int index, Lock lock, Condition condition) {
this.index = index;
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
for (int i = 0; i < Main2.total; i++) {
lock.lock();
System.out.print(Main2.name[index]);
Main2.state++;
if (Main2.state == Main2.name.length) {
Main2.state = 0;
condition.signalAll();
System.out.println("-" + i);
} else {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
}
}
方式四
每次一个线程都不停的判断当前是否该自己输出,若是则加锁输出并将当前值加一,否则继续判断,直到输出总次数满足要求(40);
//
public class Main3 {
public static void main(String[] args) {
new Thread(new T3(0)).start();
new Thread(new T3(1)).start();
new Thread(new T3(2)).start();
new Thread(new T3(3)).start();
}
}
class T3 implements Runnable{
private volatile static int curr = 0;
//private static int curr = 0;
private static final Object obj = new Object();
private int num;
public T3(int num) {
this.num = num;
}
@Override
public void run() {
while(curr < 40){
if(curr % 4 == num){
synchronized (obj){
//里面不要这个判断不影响结果
//if(curr % 4 == num) {
System.out.println(curr + "-" + num);
curr++;
//}
}
}
}
}
}