一.概述
1.JUC是JDK1.5中提供的一套并发包及其子包。包含以下:
java.util.concurrent,
java.util.concurrent.atmoic,
java.util.concurrent.lock
2.JUC中包含了5套接口:BlockingQueue、ConcurrentMap、ExecutorService,Lock和Automic
二.BlockingQueue-阻塞式队列
1.特征:阻塞、FIFO(先进先出)
2.BlockingQueue不同于之前学习的Queue,不能进行扩容。即BlockingQueue在使用的时候指定的容量是多少就是多少
3.当队列已满时,试图放入元素的线程会被阻塞;当队列为空时,似乎获取元素的线程会被阻塞。
阻塞式队列不允许元素为空。
4.重要方法:
特征 | 抛出异常 | 返回值 | 阻塞 | 定时阻塞 |
---|---|---|---|---|
添加元素 | add- java.lang.IllegalStateException | offer-false | put | offer |
移除元素 | remove-java.util.NoSuchElementException | poll-null | take | poll |
5.常见的实现类:
- ArrayBlockingQueue-阻塞式顺序队列
a.底层依靠数组来存储数据
b.使用的时候需要指定容量
package blockingqueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//构建队列
ArrayBlockingQueue queue=new ArrayBlockingQueue<>(5);
queue.add("5");
queue.add("5");
queue.add("5");
queue.add("5");
queue.add("5");
//添加元素
//队列已满
//抛出异常,java.lang.IllegalStateException
//queue.add("a");
//返回false
boolean r = queue.offer("b");
System.out.println(r);
//产生阻塞
//queue.put("c");
//定时阻塞
boolean re = queue.offer("d", 5, TimeUnit.SECONDS);
System.out.println(re);
System.out.println(queue);
}
}
- LinkedBlockingQueue-阻塞式链式队列
a.底层依靠单向节点来存储数据
b.在使用的时候可以指定容量也可以不指定,如果指定了容量,则容量不可变。如果没有指定容量,则容量为Integer.Max_VALUE,即2^31-1;此时因为这个容量相对较大,一般认为队列是无限的。
package blockingqueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class LinkeBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue queue=new LinkedBlockingQueue<>();
//队列为空
//抛出异常java.util.NoSuchElementException
//System.out.println(queue.remove());
//返回null
//System.out.println(queue.poll());
//产生阻塞
//System.out.println(queue.take());
//定时阻塞
System.out.println(queue.poll(5, TimeUnit.SECONDS));
}
}
- PriorityBlockingQueue-具有优先级的阻塞式队列
a.在使用的时候可以不指定容量。如果不指定,则默认初始容量为11-在容量不够,会进行扩容
b.底层依靠数组存储元素
c.PriorityBlockingQueue会对放入其中的元素进行排序,要求元素对应的类必须实现Comparable接口,覆盖compareTo方法
d.如果需要给队列单独指定比较规则,那么可以传入Comparator对象
e.迭代遍历不保证排序
package blockingqueue;
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
PriorityBlockingQueue queue=new PriorityBlockingQueue<>();
queue.put("Amy");
queue.put("Maray");
queue.put("Peter");
queue.put("Bob");
for(int i=0;i<4;i++){
System.out.println(queue.take());
}
PriorityBlockingQueue studnets=new PriorityBlockingQueue<>();
studnets.put(new Studnet("Amy",98,18));
studnets.put(new Studnet("Bob",48,17));
studnets.put(new Studnet("Cindy",85,20));
studnets.put(new Studnet("Lue",56,22));
studnets.put(new Studnet("Maray",100,18));
for(int i=0;i<5;i++){
System.out.println(studnets.take());
}
System.out.println("-----------------------------------------------");
//需要给队列单独指定比较规则
PriorityBlockingQueue studnet2=new PriorityBlockingQueue<>(
5,(s1,s2)->s1.getAge()-s2.getAge());
studnet2.put(new Studnet("Amy",98,18));
studnet2.put(new Studnet("Bob",48,17));
studnet2.put(new Studnet("Cindy",85,20));
studnet2.put(new Studnet("Lue",56,22));
studnet2.put(new Studnet("Maray",100,18));
for(int i=0;i<5;i++){
System.out.println(studnet2.take());
}
}
}
class Studnet implements Comparable{
private String name;
private int age;
private int score;
public Studnet(String name, int score,int age) {
this.name = name;
this.score = score;
this.age=age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Studnet{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
//按照分数进行排序
//在这个方法指定比较规则
//升序 this-o
//降序 o-this
@Override
public int compareTo(Studnet o) {
return o.score-this.score;
}
}
- SynchronousQueue-同步队列
a.不需要指定容量,容量默认为1且只能为1 - 扩展:BlockingDeque-阻塞式双向队列-允许两端存放两端拿
三.ConcurrentMap-并发映射
1.ConcurrentMap是JDK1.5提供的一套用于应对高并发以及保证数据安全的映射机制。
2.ConcurrentMap包含了ConcurrentHashMap和ConcurrentNavigableMap
3.ConcurrentHashMap-并发哈希映射
1)底层是基于数组加链表来实现的。数组的每一个位置称之为桶,每一个桶中维系一个链表。
2)如果不指定,默认情况下,初始容量为16,默认加载因子是0.75,扩容的时候是在原来的基础上增加一倍。
3)ConcurrentHashMap的最大容量(最大桶数)是2^30。
4)无论指定初始容量是多少,那么经过计算,最终容量一定是2^n的形式。
5)从JDK1.8开始,ConcurrentHashMap引入了红黑树的机制。当ConcurrentHashMap桶中的元素数量达到8个,会将这个桶中的链表扭转成为一个红黑树;如果桶中的元素数量不足7个的时候,会将这个桶中的红黑树再扭转会链表。在ConcurrentHashMap中,使用红黑树的前提是容量>=64
6)转化红黑树的前提是容量为>=64的原因如下:
假设现在容量为16,当其中桶中的链表数量达到了8个,这时需要将其转化为红黑树,但是这时候concurrentHashMap进行了插入操作,这时同样也达到了扩容的条件,这时需要同步进行红黑树的转化以及rehash操作,产生了资源冲突。所以给定前提为容量>=64,这时扩容概率不会很大。
7)红黑树(Red-Black Tree)
- 本质上是一种自平衡二叉查找树
- 二叉查找树的特征:
a.左子树小于根,右子树大于根
b.没有相等的节点 - 特征:
a.所有节点非红即黑
b.根节点必须是黑节点
c.红节点的子节点必须是黑色的
d.最底层的叶子节点必须是黑色的空节点
e.从根节点到任意一个叶子节点经过的路径的黑色节点个数一致,即黑节点高度相同
f.新添的节点颜色必须是红色的 - 红黑树的修正-前提:父子节点为红
a.叔父节点为红,那么将父节点和叔父节点涂黑,祖父节点涂红
b.叔父节点为黑,且当前节点为右子叶,则以当前节点为轴进行左旋
c.叔父节点为黑,且当前节点为左子叶,则以当前节点为轴进行右旋 - 在红黑树中,每添加一个元素,都需要考虑这棵树是否需要修正
-
红黑树的查询时间复杂度为O(log n)
8)ConcurrentHashMap是一个异步线程安全的映射-支持并发。不同于Hashtable,ConcurrentHashMap采用了分段/桶锁机制来保证线程安全。----宏观同步,微观异步(映射异步,桶同步)
HashMap:异步线程不安全
Hashtable:同步线程安全-凡是对外提供的方法都是同步方法,静态方法锁对象是当前类的字节码对象,非静态方法的锁对象是this
9)线程在使用锁的时候,会产生非常大的开销(线程状态切换、线程的上下文调度、CPU资源的切换等)
因此在JDK1.8中,引入了一套无锁算法CAS(Compare And Swap 比较和交换)-CAS过程中涉及到线程的重新调度问题,所以CAS需要结合具体的CPU内核架构实现。目前市面上几乎所有的CPU内核都是支持CAS的。Java中的CAS底层是依靠C语言实现的。
10)ConcurrentHashMap的用法是和HashMap一致。
4.ConcurrentNavigableMap-并发导航映射
1)ConcurrentNavigableMap提供了用于截取子映射的方法--headMap、tailMap、subMap
package concurrentmap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentNavigableMapDemo {
public static void main(String[] args) {
//实现类ConcurrentSkipListMap-并发跳跃表映射
ConcurrentNavigableMap map=new ConcurrentSkipListMap<>();
map.put("Amy",98);
map.put("Bob",78);
map.put("Jack",75);
map.put("Rose",88);
map.put("Tony",45);
System.out.println(map);
//从头开始截取到指定位置
System.out.println(map.headMap("Jack"));
//从指定位置截取到尾部
System.out.println(map.tailMap("Bob"));
//截取指定范围的数据
System.out.println(map.subMap("Bob","Rose"));
}
}
2)ConcurrentNavigableMap本身是一个接口,在JDK中提供了唯一的实现类ConcurrentSkipListMap-并发跳跃表映射--底层是基于跳跃表实现的
3)跳跃表:
原理参考连接:https://blog.csdn.net/qpzkobe/article/details/80056807
- 针对有序列表来使用
- 适合于读多写少的场景
- 跳跃表可以进行多层提取,但是最后一层的元素个数不能少于2个
- 典型的“以空间换时间”的产物
- 当新增元素的时候,这个元素是否要提取到上层跳跃表中遵循“抛硬币”原则
- 跳跃表的时间复杂度为O(log n),空间复杂度为O(n)
四.ExecutorService-执行器服务
1.本质上是一个线程池。意义:减少线程的创建和销毁,减少服务器资源的浪费,做到线程的复用
2.线程池在刚定义的时候是空的,没有任何线程。
3.如果接收到一个请求,线程池中就会创建一个线程(core-thread -核心线程)用于处理这个请求
4.核心线程用完之后不会销毁而是会去等待下一个请求。
5.在定义线程池的时候需要去给定核心线程的数量。
6.在核心线程达到指定数量之前,每次来的请求都会触发创建一个新的核心线程。
7.如果核心线程被全部占用,那么后来的线程将会放到工作队列(work queue)中临时存储。工作队列本质上是一个阻塞式队列
8.如果工作队列被全部占用,那么后来的请求会被交给一个临时线程(temproary thread)来处理
9.在定义线程池的时候需要给定临时线程的数量
10.临时线程在处理完请求之后,会存活指定的一段时间。如果在这段时间内接受到新的请求,那么临时线程会继续处理新的请求而暂时不会被销毁;如果超过这段时间临时线程没有接收到新的请求,那么这个临时线程就会被销毁。
11.如果临时线程被全部占用,那么后来的请求会被交给拒绝执行处理器(RejectedExecutionHandler)来进行拒绝处理。
12.代码:
submit和execute的区别:
execute()用于提交Runnable线程
submit()既可以提交Runnable线程也可以提交Callable线程
package executorservice;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo {
public static void main(String[] args) {
//构建一个线程池
/**
*int corePoolSize---核心线程数量
*int maximumPoolSize---最大线程数量=核心线程数+临时线程数
*long keepAliveTime---临时线程存活时间
*TimeUnit unit---时间单位
*BlockingQueue workQueue---工作队列
*RejectedExecutionHandler handler---拒绝执行处理器--如果有具体的拒绝流程,需要覆盖这个接口
*/
ExecutorService es=new ThreadPoolExecutor(5,
10,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
//实际过程中,会有一套明确的拒绝流程
(r,e)-> System.out.println("拒绝执行线程")
);
//new Thread(new ExecutorThread()).start();
//可以通过线程池来执行这个线程
/**
* submit和execute的区别:
* execute()用于提交Runnable线程
* submit()既可以提交Runnable线程也可以提交Callable线程
*/
//es.execute(new ExecutorThread());
es.submit(new Thread());
//关闭线程池
es.shutdown();
}
}
class ExecutorThread implements Runnable{
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
13.Callable
1)Callable是JDK1.5中提供的一套用于定义线程的方式,通过泛型来定义返回值类型
2)创建Callable线程的两种方式:
package executorservice;
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//方式一:将Callable包装成Runnable,通过Thread来启动
//Callable->FutureTask->RunnableFuture->Runnable
FutureTask f=new FutureTask<>(new CallableThread());
new Thread(f).start();
//获取指定结果
System.out.println(f.get());
//方式二:通过线程池来启动Callable线程
ExecutorService es=new ThreadPoolExecutor(5,10,5,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5));
Future f1 = es.submit(new CallableThread());
System.out.println(f1.get());
es.shutdown();
}
}
//泛型定义的是返回值类型
class CallableThread implements Callable{
@Override
public String call() throws Exception {
return "SUCCESS";
}
}
3)Runnable和Callable比较:
比较 | Runnable | Callable |
---|---|---|
返回值 | 没有返回值 | 通过泛型来定义返回值 |
启动方式 | 1.通过Thread直接启动 2.通过线程池的execute或者submit来启动 |
1.包装成Runnable之后通过Thread来启动 2.通过线程池的submit方法来启动 |
异常机制 | 不允许抛出异常,一旦出现异常需要立即捕获处理,就没有办法利用全局机制来进行处理 | 允许抛出异常,意味着可以选择用全局机制(例如Spring中的异常通知)来统一处理异常 |
14.预定义的线程池
package executorservice;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceDemo2 {
public static void main(String[] args) {
//预定义的线程池
/**
* newCachedThreadPool特点:
* 1.没有核心线程,全部都是临时线程
* 2.临时线程的数量为Integer.MAX_VALUE,即2^31-1
* 一台服务器能够承载的线程数量远低于这个值
* 所以此时认为这个线程池能够处理无限多的请求
* 3.临时线程的存活时间是一分钟
* 4.工作队列是一个同步队列(容量为1)
*/
//大池子小队列
//适合于高并发的短任务场景,例如即时通信
//不适合于长任务场景
ExecutorService es= Executors.newCachedThreadPool();
/**
* newFixedThreadPool特点:
* 1.没有临时线程,全部都是核心线程
* 2.工作队列是一个阻塞式链式队列,且容量为Integer.MAX_VALUE,
* 此时认为这个线程池能够处理无限多的请求
*/
//小池子大队列
//适合于并发低的长任务场景,例如文件下载
//不适合高并发的短任务的场景
ExecutorService es1=Executors.newFixedThreadPool(5);
}
}
15.ScheduledExecutorService--定时调度执行器任务。能够起到定时调度的效果
package executorservice;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses= Executors.newScheduledThreadPool(5);
//延时执行
//ses.schedule(new ScheduleThread(),5, TimeUnit.SECONDS);
//每隔5秒执行一次
//从上次的开始来计算下一次的启动时间
//实际间隔时间=max(指定时间,线程执行时间)
//ses.scheduleAtFixedRate(new ScheduleThread(),0,5,TimeUnit.SECONDS);
//每隔5秒执行一次
//从下一次的结束来计算下一次启动时间
//实际间隔时间=指定时间+线程执行时间
ses.scheduleWithFixedDelay(new ScheduleThread(),0,5,TimeUnit.SECONDS);
}
}
class ScheduleThread implements Runnable{
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
16.ForkJoinPool分叉合并池
- 分叉:将一个大的任务拆分成多个小的任务交给多个线程来执行
- 合并:将拆分出去的小的任务的计算结果来进行汇总
- 求1-100000000000L的和
package executorservice;
import java.util.concurrent.*;
public class ForkJoinPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start=System.currentTimeMillis();
//求1-100000000000L的和
//主函数所在的类默认是一个线程类-主线程
//一个线程只能落到一个CPU核上
//运行时间:39780
/*long sum=0;
for(long i=1;i<=100000000000L;i++){
sum+=i;
}
System.out.println(sum);*/
//运行时间:23613
ForkJoinPool pool=new ForkJoinPool();
Future f = pool.submit(new Sum(1, 100000000000L));
System.out.println(f.get());
pool.shutdown();
long end=System.currentTimeMillis();
System.out.println(end-start);
}
}
class Sum extends RecursiveTask{
private long start;
private long end;
public Sum(long start, long end) {
this.start = start;
this.end = end;
}
//分叉合并的逻辑就是覆盖在这个方法中
@Override
protected Long compute() {
//拆分,如果拆分出去的范围较大,那么继续拆分
//如果拆分出去的范围较小,那么将这个小的范围的数字进行求和
if(end-start<=10000){
long sum=0;
for(long i=start;i<=end;i++){
sum+=i;
}
return sum;
}else {
long mid=(start+end)/2;
Sum left=new Sum(start,mid);
Sum right=new Sum(mid+1,end);
//分叉
left.fork();
right.fork();
//合并
return left.join()+right.join();
}
}
}
- 在数据量比较小的时候,使用循环的效率反而比较高,数据量越大,分叉合并的效率越高
- 分叉合并通过大量的线程抢占CPU,从而能够有效地提高CPU的利用率,可能就会导致其他线程被挤占。因此在实际生产过程中,慎用分叉合并。如果需要使用分叉合并,放在相对空闲的时间来执行
- 在分叉合并中,当一个核上的任务执行完毕之后,这个和不会空闲下来,而是随机扫描一个核,从这个被扫描的核的任务队列尾端来“偷取”一个任务回来执行--“work-stealing”(工作窃取)策略
五.Lock-锁
1.Lock是JDK1.5提供的一套锁机制,在实际生产过程中更推荐使用Lock代替synchronized---相对而言,Lock比synchronized更加灵活。
package Lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
static int i=0;
public static void main(String[] args) throws InterruptedException {
Lock lock=new ReentrantLock();
new Thread(new Add(lock)).start();
new Thread(new Add(lock)).start();
//main所在的类是一个线程类-主线程
//这个线程在执行过程中需要启动两个Add的线程
//这两个Add线程在启动过程中,主线程会抢占CPU继续执行
//考虑:主线程即使抢占到CPU也需要阻塞
Thread.sleep(3000);
System.out.println(i);
}
}
class Add implements Runnable{
private Lock lock;
public Add(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
//加锁
lock.lock();
for (int i = 0; i < 10000; i++) {
LockDemo.i++;
}
//解锁
lock.unlock();
}
}
2.ReentrantLock-重入锁
a.重入锁:当锁资源被释放之后,这个锁资源可以再次被线程占用
b.非重入锁:当锁资源被释放之后,不能被再次使用--非重入锁更多的是在校验中使用
3.大部分排他锁和自旋锁
- 自旋锁也是排他锁
- 对于其他的陪他锁而言,当一个线程占用锁对象之后,其他的线程会陷入阻塞状态,持续等待。当锁资源被释放之后,被阻塞的线程需要被唤醒之后才能抢占,这个过程中就涉及到了线程的状态的变化
- 自旋锁的特点在于,当发现锁资源被占用之后,线程不会陷入阻塞,而是持续判断锁资源是否被释放
- 自旋锁因为没有状态的转化,所以效率相对要高一些;但是相对而言,自旋锁会持续占用CPU资源
4.ReadWriteLock-读写锁
- 读锁:允许多个人同时读,不允许写入--本质上是共享锁
- 写锁:只允许一个人写,不允许读--本质上是排他锁
//获取写锁
ReadWriteLock rw=new ReentrantReadWriteLock();
Lock lock=rw.writeLock();
5.公平策略和非公平策略
- 在资源有限的情况下,虽然理论上各个线程抢占的几率相等,但是实际上各个线程的抢占次数并不相等,这种现象称之为非公平策略
-
在公平策略的前提下,各个线程并不能直接抢占资源,而是需要抢占入队顺序。此时,各个线程的执行次数大致是相等的
- 公平策略需要涉及到大量线程调度的问题,所以相对而言,非公平策略的效率更高。
- synchronized、Lock默认是非公平的
//非公平的
ReadWriteLock rw=new ReentrantReadWriteLock(false);
//公平的
ReadWriteLock rw=new ReentrantReadWriteLock(true);
6.其他
- CountDownLatch:闭锁/线程递减锁。对线程进行计数,在计数归零之前,线程会陷入阻塞;直到计数归零之后,会自动放开阻塞-上一组线程结束需要开启下一组线程
package Lock;
import java.util.concurrent.CountDownLatch;
/**
* 案例:考试
* 考官和考生到达考场之后,开始考试
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl=new CountDownLatch(7);
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Teacher(cdl)).start();
new Thread(new Teacher(cdl)).start();
//需要等上面的线程执行完毕之后才能继续执行下面的逻辑
//在上面的线程执行完毕之前,当前主线程需要阻塞
cdl.await();
System.out.println("开始考试!!!");
}
}
class Teacher implements Runnable{
private CountDownLatch cdl;
public Teacher(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//模拟:考官走到考场时间
try {
Thread.sleep((long)(Math.random()*10000));
System.out.println("考官到达考场~");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Student implements Runnable{
private CountDownLatch cdl;
public Student(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//模拟:考生走到考场时间
try {
Thread.sleep((long)(Math.random()*10000));
System.out.println("考生到达考场~");
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- CyclicBarrier:栅栏。对线程进行计数。在计数归零之前,线程会陷入阻塞。直到线程计数归零,会自动放开阻塞。-所有线程到达同一个点之后再分别继续执行
package Lock;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 案例:跑步比赛
* 运动员先到起跑线,人齐之后,听到枪响之后在跑出去
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cb=new CyclicBarrier(6);
new Thread(new Runner(cb),"1号").start();
new Thread(new Runner(cb),"2号").start();
new Thread(new Runner(cb),"3号").start();
new Thread(new Runner(cb),"4号").start();
new Thread(new Runner(cb),"5号").start();
new Thread(new Runner(cb),"6号").start();
}
}
class Runner implements Runnable{
private CyclicBarrier cb;
public Runner(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
try {
//模拟运动员走到起跑线
Thread.sleep((long) (Math.random()*10000));
String name=Thread.currentThread().getName();
System.out.println(name+"运动员走到了起跑线");
//先到起跑线的人需要等待,直到人齐了,听到枪响之后再跑
//阻塞,减少计数--计数归零会自动放开阻塞
cb.await();
System.out.println(name+"跑了出去~");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
- Exchanger
:交换机,用于交换两个线程之间的信息。
package Lock;
import java.util.concurrent.Exchanger;
/**
* 案例:购物
* 一手交钱一手交货
*/
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger ex=new Exchanger<>();
new Thread(new Seller(ex)).start();
new Thread(new Consumer(ex)).start();
}
}
class Consumer implements Runnable{
private final Exchanger ex;
public Consumer(Exchanger ex) {
this.ex = ex;
}
@Override
public void run() {
String info="钱";
//挑好东西之后,需要付款,商家需要将商品交换给消费者
String msg= null;
try {
msg = ex.exchange(info);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者收到商家给的:"+msg);
}
}
class Seller implements Runnable{
private final Exchanger ex;
public Seller(Exchanger ex) {
this.ex = ex;
}
@Override
public void run() {
String info="商品";
//商家将商品交付给消费者之后,需要收到消费者的付款
try {
String msg = ex.exchange(info);
System.out.println("商家收到消费者给的:"+msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- Semaphore:信号量。在执行指定逻辑之前,线程需要先获取信号。当信号被全部获取完,那么后来的线程就会被阻塞;直到有信号被释放,那么被阻塞的线程才会获取信号执行逻辑-在实际生产过程中,信号量适用于限流的
package Lock;
import java.util.concurrent.Semaphore;
/**
* 案例:去餐馆吃饭
* 餐馆中的桌子的数量有限。如果所有的桌子被占用,那么后来的人就会被阻塞
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//6个信号->6张桌子
Semaphore s=new Semaphore(6);
for (int i = 0; i < 10; i++) {
new Thread(new Eater(s)).start();
}
}
}
class Eater implements Runnable{
private Semaphore s;
public Eater(Semaphore s) {
this.s = s;
}
@Override
public void run() {
//占用一张桌子
//桌子->信号
try {
//获取一个信号
s.acquire();
System.out.println("来了一波客人,占用了一张桌子");
//模拟用餐时间
Thread.sleep((long) (Math.random()*10000));
System.out.println("客人用餐完毕离开,空出来一张桌子");
//一张桌子就空出来相当于一个信号被释放
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
六.Atomic操作-原子性操作
1.原子性操作实际上针对属性提供了大量的线程安全的方法。在jdk1.8中,采用了CAS+volatile机制来保证属性的线程安全。
2.volatile是Java中的关键字之一,是Java提供的一种轻量级的线程间的通信机制
- 保证线程的可见性。当共享资源发生变化的时候,其他线程能够立即感知到这种变化并且做出对应的操作,这个过程称之为可见性
- 不保证线程的原 子性。原子性指的是线程的执行过程不可分割。换而言之,就是线程的执行过程不会被打断不会被抢占。加锁实际上保证的线程的原子性。
- 禁止指令重排。指令重排指的是预先定义的顺序和指令的实际执行顺序执行不一致。 指令重排可能发生在每一步(java-class-System-CPU),但是注意,每一步过程中,发生指令重排的概率不足百万分之一。指令重排不能违背happen-before(先发生)原则-使用的变量必须先产生。在多线程的情况下,执行完全相同的代码可能会因为指令重排获取到不同的结果,这种现象称之为结果的二相性。