目录
3 Java并发包—ConcurrentHashMap和多线程执行控制
码字不易,喜欢就点个关注❤,持续更新技术内容。相关资料请私信。
相关内容:
1 多线程下数据可见性问题
1.1 数据可见性问题介绍
介绍
在多线程并发编程下,线程修改共享资源变量,会出现线程间变量的可见性问题(不可见性)。即一个线程修改共享变量的值后,会出现其他线程看不到变量最新值的情况。
理解原因之前先简单来了解Java内存模型(JMM),JMM是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽了底层不同计算机的区别。JMM描述了线程访问(读写)内存中共享变量规则和细节。
JMM规定:
首先所有共享变量(实例变量和类变量)都存储于主内存。
每一个线程都存在自己的工作内存,其保留主内存的变量副本。(有独立的内存空间就不会产生干扰,性能更高)
线程在工作内存中完成对变量副本的所有操作(读写),而不能直接操作主内存中的变量。
所以不同线程间不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存中转。
示例
下面定义了两个线程,一个线程修改自身变量,另一个线程获取前面线程中的数据进行判断后反馈:
public class VolatileDemo01 {
public static void main(String[] args) {
// 1.启动线程,把线程对象中的flag改为true。
VolatileThread t1 = new VolatileThread("自定义线程1");
t1.start();
MyThread t2 = new MyThread("自定义线程2", t1);
t2.start();
}
}
// 修改工作内存中的变量
class VolatileThread extends Thread {
// 定义成员变量
// volatile可以实现变量一旦被子线程修改,其他线程可以马上看到它修改后的最新值
private volatile boolean flag = false;
private int sum = 0 ;
public VolatileThread(String name) {
this.setName(name);
}
public int getSum() {
return sum;
}
public boolean isFlag() {
return flag;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
// 模拟经过现实开发中的一些业务之后将flag的值更改为true
this.flag = true;
this.sum += 5;
System.out.println(Thread.currentThread().getName() + "执行0+5="+sum);
System.out.println("flag = true");
}
}
// 获取前面线程中的数据进行判断后反馈
class MyThread extends Thread{
private VolatileThread vt;
public MyThread(String name, VolatileThread vt) {
// public Thread(String name):父类的有参数构造器
super(name); // 调用父类的有参数构造器初始化当前线程对象的名称!
this.vt = vt;
}
// 2.重写run()方法
@Override
public void run() {
while (true) {
if(vt.isFlag()){
if(vt.getSum() == 5) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "得知"+vt.getName()+"的加和结果为:"+vt.getSum());
}else {
System.out.println(Thread.currentThread().getName() + "未得知"+vt.getName()+"的加和结果");
}
}
}
}
}
根据运行结果得知对于布尔类型的变量会产生可见性问题,需要加上volatile关键字,而普通类型成员变量不会产生可见性问题(也可能会出现),所以加上volatile更好。
1.2 数据可见性问题解决
有两种常见方案:
在共享变量上加volatile关键字:保证数据的可见性,但是不保证原子性。只能修饰实例变量和类变量。
当变量改变或修改后同步到主内存, 通知其他线程的工作内存该变量的值不是最新的,然后从主内存中提取最新值,就保证了在多线程环境下共享数据的可见性。 但在多线程并发随机性的环境下,volatile不能保证变量的安全。(不保证原子性:不保证数据一致)在访问变量的代码上加同步锁:保证数据的可见性,也保证了原子性。可以修饰方法,以及代码块。
a.线程获得锁 b.线程清空自己的工作内存 c.再从主内存拷贝最新共享变量的值到工作内存成为副本,也保证共享变量的一致性
但加同步锁总会降低响应速度,消耗时间。所以还是要根据业务是否需要线程安全来选择可见性问题的解决方案。
2 操作原子性问题
2.1 操作原子性问题介绍
介绍:原子性要求一批操作是一个整体(事务),要么都执行成功,要么都不执行。多线程的整体操作不具备原子性,导致变量的结果与操作预期的结果不一致。即事务操作的原子性问题会造成数据的不安全。
// 在线程类的执行方法中对变量count进行十次加和,每次加一
// 开启10000个线程,相当于进行了10000*10次加和,和为十万
class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
Runnable target = new VolatileAtomicThread() ;
// 开启1000个线程对执行这一个任务。
for(int x = 0 ; x < 1000 ; x++) {
new Thread(target).start();
}
}
}
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
private volatile int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,10次
for(int x = 0 ; x < 10 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
但是对变量count进行10000次操作下来结果并不是操作的预期结果,所以这批操作出现了原子性问题,导致数据结果不一致。
而且发现,如果开启的线程越多,产生原子性问题的概率就越大,就会导致数据结果不一致。因为当多个线程有可能同时从主内存中读取到相同的变量,进行加和后返回相同的结果值,相当于这几个线程操作变成了一次线程操作,其他线程没有成功执行。
另外,volatile只负责报告线程操作变量后的最新值。volatile与原子性操作没有关系,也不能解决。原子性只针对原子性操作。
2.2 操作原子性问题解决
同步机制
为线程为操作变量的执行方法添加同步锁机制,即保证了数据的可见性,同时保证操作的原子性。因为同步机制限制了线程操作的顺序,不能同时读写一个变量,每个操作都成功执行了,那么事务整体的操作就具有原子性。
但是加锁机制性能较差,我们使用原子类。
原子类CAS机制
JDK5开始提供的并发包中,包含Atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。即就是保证了事务操作的原子性。
原理:CAS机制。即Compara And Swap比较再交换。CAS机制当中使用了3个基本操作数:内存地址、旧值、新值。原理如下过程模拟:
在内存地址V中存储了一个值为9,假设线程1打算将值加1,此时对于线程1来说旧值为9,新值为10。
此时线程2也同时读到了旧值9,并在自己的工作内存中进行了加1并且抢先于线程1提交了新值。
然后线程1提交时发现原来的旧值与现在内存地址中的值不相等(Compara),提交失败。并且线程1重新获取现在内存地址的值作为旧值继续加和,这个由于不一致而重新尝试的过程称为自旋。直到成功执行,才将内存地址的值交换为新值(Swap)。
CAS机制与同步锁机制
CAS机制与同步锁机制都可以保证多线程并发环境下共享数据的安全性。但两者又有不同,CAS机制与同步锁机制分别是乐观和悲观的应对方式:
悲观应对方式:假设最坏的情况,也就是线程每次在访问资源时认为有其他线程也在同时访问。所以直接为资源上同步锁,只允许获取锁的线程访问。
乐观应对方式:假设最好的情况,也就是线程每次在访问资源时认为没有其他线程在干扰。所以不上锁,但还是每次都进行了判断,如果出现坏的情况(数据不一致),那么就重新进行读写,直到操作正确无误地成功执行。
使用原子类的CAS机制就是以这种乐观的方式,保证了操作的原子性,不用加同步锁,性能高效。
示例
以AtomicInteger原子类型为例实现原子更新操作:
操作整型的原子类,构造器后方法:
public AtomicInteger():初始化一个默认值为0的原子型Integer public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer int get():获取值 int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。 int incrementAndGet():以原子方式将当前值加1,注意,这里返回的是自增后的值。 int addAndGet(int data):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 int getAndSet(int value):以原子方式设置为newValue的值,并返回旧值。
class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread线程对象
Runnable target = new VolatileAtomicThread() ;
// 开启100个线程对执行这一个任务。
for(int x = 0 ; x < 1000 ; x++) {
new Thread(target).start();
}
}
}
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
AtomicInteger atomicInteger = new AtomicInteger(0);
private volatile int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 10 ; x++) {
System.out.println("count =========>>>> " + atomicInteger.getAndIncrement());
}
}
}
3 Java并发包—ConcurrentHashMap和多线程执行控制
并发包:
从JDK5开始,提供了专门针对多线程操作及并发安全的并发包JUC,性能优异且线程安全。
3.1 ConcurrentHashMap
介绍
-
Map集合中的经典集合HashMap,它是线程不安全的,但性能最好。如果业务要求线程安全就不能用这个HashMap做Map集合了,否则业务会崩溃。(多线程并发的随机性,同时进入判断,导致重复插入或删除,丢失数据或报错,出现操作的原子性问题。没有线程安全时推荐使用)。
-
Map集合中还有Hastable,它是线程安全的Map集合,但是性能较差。(已经淘汰,因为方法清一色加锁,锁定整张哈希表,简单粗暴)。
-
为了线程安全且性能好,可以用并发包中的ConcurrentHashMap,综合性能好,不仅线程安全而且性能好,又新又好(JDK5)。(只对局部锁定,即只锁定当前操作的键值元素的位置,其他元素不锁定)。
示例
public class ConcurrentHashMapDemo {
// 定义一个静态的HashMap集合,只有一个容器。
public static Map<String,String> map = new HashMap<>();//线程不安全
// public static Map<String,String> map = new Hashtable<>();//线程安全,性能较差
// public static Map<String,String> map = new ConcurrentHashMap<>();//线程安全,性能得到极大提升
public static void main(String[] args) throws InterruptedException {
// HashMap线程不安全演示。
// 需求:多个线程同时往HashMap容器中存入数据会出现安全问题。
// 具体需求:提供2个线程分别给map集合加入50万个数据!
Thread t1 = new AddMapDataThread("线程1");
Thread t2 = new AddMapDataThread("线程2");
t1.start();
t2.start();
try {
t1.join();//让t1跑完,当前主线程不能竞争t1的CPU
t2.join();//让t2跑完,当前主线程不能竞争t2的CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
// new AddMapDataThread().start();
// new AddMapDataThread().start();
//
// //休息10秒,确保两个线程执行完毕
// Thread.sleep(1000 * 4);
//先保证t1和t2线程运行结束再打印集合大小
System.out.println("Map大小:" + map.size());
}
}
class AddMapDataThread extends Thread{
public AddMapDataThread(String name){
this.setName(name);
}
@Override
public void run() {
long start = System.currentTimeMillis();
for(int i = 0 ; i < 1000000 ; i++ ){
ConcurrentHashMapDemo.map.put(Thread.currentThread().getName()+"键:"+i , "值"+i);
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "耗时" + (end-start)/1000.0 + "s");
}
}
总结
执行情况是HashMap线程不安全,其他的加锁的线程安全。耗时HashMap < Hastable < ConcurrentHashMap。
注意:这是因为我们进行的是顺序插入,每次插入时Hashtable锁定整张表,而ConcurrentHashMap要锁定当前插入的元素位置,不断推移,所以在顺序插入时Hashtable稍快,但是现实业务中一般都不是顺序插入,ConcurrentHashMap还是综合性能最好,建议多线程并发下使用。
3.2 CountDownLatch
介绍
计数器,控制多线程执行,类似于线程通信协作。CountDownLatch可以实现线程等待,需要其他线程的一次或者多次唤醒才能继续向下执行。从而可以实现控制线程执行的流程。(类似于线程通信中唤醒其他线程,等待自己,也需要别人唤醒)
构造器和方法 | 说明 |
---|---|
public CountDownLatch(int count) | 初始化需要减掉几次才唤醒 |
public void await() throws InterruptedException | 让当前线程等待,必须减完初始化的数字才可以被唤醒,否则进入无限等待 |
public void countDown() | 唤醒次数减1 |
示例
下面模拟在线程A执行的过程中等待,等待其他线程执行完毕并调用countDown()减掉一次唤醒步数。
public class CountDownLatchDemo {
public static void main(String[] args) {
//创建1个唤醒部步数计数器:用来控制 A , B线程的执行流程的
//初始化监督者,监督线程挂起等待,只需减掉一次就能唤醒
CountDownLatch down = new CountDownLatch(1);
new ThreadA(down).start();
new ThreadB(down).start();
}
}
class ThreadA extends Thread{
private CountDownLatch down;
public ThreadA(CountDownLatch down){
this.down = down;
}
@Override
public void run() {
System.out.println("线程A执行");
try {
// A线程你进入等待(挂起),唤醒B线程执行
down.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程A继续执行");
}
}
class ThreadB extends Thread{
private CountDownLatch down;
public ThreadB(CountDownLatch down){
this.down = down;
}
@Override
public void run() {
System.out.println("线程B执行");
down.countDown(); // 执行完毕,唤醒次数减1,A线程被唤醒。
}
}
效果:
3.3 CyclicBarrier
介绍
循环屏障,控制多线程执行,可以实现多线程中,某个线程在等待其他线程执行完毕以后触发执行。其他线程执行完毕全部到达屏障之后唤醒屏障中的线程任务执行。
构造器和方法 | 说明 |
---|---|
public CyclicBarrier(int number, Runnable barrierAction) | number代表多少个线程的执行。 barrierAction代表到达执行屏障就触发的线程任务对象,由其中一个线程执行。 |
public int await() | 每个线程调用await方法表示自己已到达屏障,然后阻塞 |
使用场景:CyclicBarrier可用于多线程执行任务,最后触发其中一个 线程执行结果汇总。
示例
下面模拟五个学生写试卷,写完后其中某个学生负责收试卷(五个线程到达屏障,最后在触发其中某一个线程任务执行汇总任务):
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 1.创建一个任务循环屏障对象。
/**
* 参数一:代表多少个线程的执行。
* 参数二:到达执行屏障就开始触发其中一个线程执行线程任务。
*/
CyclicBarrier cb = new CyclicBarrier(5 , new SummarizingRunnable());
//开启线程,五个线程开始写试卷
new Student(cb, "泰山").start();
new Student(cb, "衡山").start();
new Student(cb, "华山").start();
new Student(cb, "恒山").start();
new Student(cb, "嵩山").start();
}
}
// 任务类:开始开会的任务
class SummarizingRunnable implements Runnable{
@Override
public void run() {
System.out.println("全部学生写完,"+Thread.currentThread().getName()+"开始收试卷!");
}
}
// 员工类
class Student extends Thread{
private CyclicBarrier cb ;
public Student(CyclicBarrier cb, String name) {
super(name);
this.cb = cb;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("学生:"+Thread.currentThread().getName()+"在写试卷...");
cb.await(); // 自己做完了,唤醒循环屏障中的线程!
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果:
3.4 Semaphore
介绍
信号量,限制对资源并发访问的线程数量,acquire()和release()方法之间的代码为"同步代码",允许指定数量线程并发执行,其实就是限制线程并发度。(semaphore与synchronized的区别就是一个是控制线程对资源访问的并发度,多个线程在执行;一个是同步线程并发访问资源,只能一个线程执行)(了解就行)
构造器和方法 | 说明 |
---|---|
public Semaphore(int permits) | permits表示许可线程并发占锁的数量 |
public Semaphore(int permits, boolean fair) | fair表示公平性,如果这个设为true的话,下次执行的线程会是等待最久的线程 |
public void acquire() throws InterruptedException | 表示获取许可 |
public void release() | 表示释放许可 |
介绍
假如一个考场座位有限,一个时间点只能5个人进行考试,以下是模拟代码:
public class SemaphoreDemo {
public static void main(String[] args) {
Service service = new Service();
for(int i = 1 ; i <= 100 ; i++ ){
new MyThread(service,"线程"+i+":").start();
}
}
}
// 任务类
class Service{
// 限制指定线程数量进入资源区访问
// acquire()和release()方法之间的代码为指定线程数量访问区
private Semaphore semaphore = new Semaphore(5);
public void showMethod(){
try {
semaphore.acquire();//当达到5个并发线程的时候就不允许其他线程进入了,当有线程出去才开放许可
long startTimer = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"进来考试");
Thread.sleep(1000); // 模拟任务执行消耗时间
long endTimer = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"结束离开,耗时:"+(endTimer-startTimer));
} catch (Exception e) {
e.printStackTrace();
}
semaphore.release();
}
}
// 线程类
class MyThread extends Thread{
private Service service;
public MyThread(Service service, String name){
super(name);
this.service = service;
}
@Override
public void run() {
service.showMethod();
}
}
3.5 Exchanger
介绍
交换者,用于线程间协作的工具类。和等待和唤醒不一样,,Exchanger可以做数据校对,用于进行线程间的数据交换。
两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
一个线程如果等不到对方的数据就会一直等待。也可以设置timeout,控制线程等待的时间。
构造器和方法 | 说明 |
---|---|
public Exchanger() | 创建交换对象 |
public V exchange(V x) | 拿V对象进行交换 |
public V exchange(V x, long timeout, TimeUnit unit) | 拿V对象进行交换,而且还可以设置该线程等待交换的最大时间 |
示例
模拟兑奖者兑换奖品,一个线程将兑奖号码交换给兑奖单位,另一个线程将做好的奖品交换给兑奖者,完成交换:
public class ExchangerDemo {
public static void main(String[] args) {
// 创建交换对象(信使)
Exchanger<String> exchanger = new Exchanger<>();
// 创建2给线程对象。
new ThreadA(exchanger,"兑奖者").start();
new ThreadB(exchanger,"兑奖单位").start();
}
}
class ThreadA extends Thread{
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger,String name) {
super(name);
this.exchanger = exchanger;
}
@Override
public void run() {
try {
//用于模拟线程超时
Thread.sleep(11000);
// 礼物A
System.out.println(Thread.currentThread().getName() + ":提交了兑奖码,等待交换奖品...");
// 开始交换,参数是交给其他线程的对象
// 如果等待了5s还没有交换它就抛出异常,结束交换
System.out.println(Thread.currentThread().getName() + ":收到"+exchanger.exchange("兑奖码", 5 , TimeUnit.SECONDS));
} catch (Exception e) {
System.err.println("兑奖者等待了5秒,没有交换到奖品,结束交换并进行了投诉!");
}
}
}
class ThreadB extends Thread{
private Exchanger<String> exchanger;
public ThreadB(Exchanger<String> exchanger,String name) {
super(name);
this.exchanger = exchanger;
}
@Override
public void run() {
try {
//用于模拟线程超时
Thread.sleep(6000);
// 礼物B
System.out.println(Thread.currentThread().getName() + ":做好了奖品,等待兑奖者来兑奖...");
// 开始交换,参数是交给其他线程的对象
// 如果等待了10s还没有交换它就抛出异常,结束交换
System.out.println(Thread.currentThread().getName() + ":收到"+exchanger.exchange("奖品", 10, TimeUnit.SECONDS));
} catch (Exception e) {
System.err.println("兑奖时间超10秒期限,不再接收兑奖!");
}
}
}
模拟兑奖单位线程超时,超过5秒后兑奖单位才进行兑奖,但兑奖者线程已经抛异常结束了:
模拟兑奖者线程超时,超过10秒后兑奖者才来兑奖,但兑奖单位线程已经抛异常结束了:
模拟兑奖单位线程和兑奖者线程都没有超时: