记录Java并发编程的知识,包括并发编程的详细介绍,并发编程解决的问题,volatile关键字,各种锁机制,synchronized的底层原理,CAS机制,AQS机制,以及JUC里面常见方法

文章目录

  • 一、什么是并发编程
  • 二、volatile关键字
  • 三、解决非原子性
    • 1.原子变量
    • 2.锁
    • 3.对象头
    • 4.synchronized的底层实现
    • 5.AQS
    • 6.ReentrantLock
  • 四、常用JUC类
    • 1.ConcurrentHashMap
    • 2.CopyOnWriteArrayList
    • 3.CountDownLatch
    • 4.CyclicBarrier

一、什么是并发编程

在计算机里面有并发与并行的概念;当我们完成一件事情的时候,我们一定对完成这件事情的时间与效率很关心;当我们使用计算机去执行命令帮助我们完成一件事情也是一样的,我们希望计算机高效的完成;根据冯诺依曼计算机模型我们可以知道计算机大体组成是:运算器,控制器,存储器,输入设备,输出设备。计算机发展过程中CPU承担了运算器与控制器的作用,内存与外存承担了存储器的功能,我们现在需要从CPU与内存,磁盘存储(外存)三者进行分析:CPU现在性能强劲,计算速度很快;内存相对于外存来讲,速度是快的,但是与CPU运算速度进行比较的话就慢了;为了缓解这三者之间的速度差异,我们使用了缓存;从外存获取必要的数据资源信息到内存,CPU从内存中获取必要的数据资源;从多核CPU进行分析,每个CPU都有属于自己的工作内存,所有这些CPU又共享一块工作内存,每个CPU是不可以访问其他CPU的工作内存的,当他们将数据在直接的工作内存中处理完毕就会将数据写入到所有CPU共享的工作内存,最后写入到我们的外存中;这个过程让CPU减少了访问外存的次数,提高了执行效率;但是也带来了问题:多个CPU同时刻分别执行任务,这就是并行的概念;单个CPU在一个时间段内分别执行任务就是并发的概念;并行是提高执行速率的一个关键,也是带来问题的关键

在单个CPU执行任务的时候会出现并发的现象,也就意味着CPU有可能不会等一个线程执行完毕就让出执行权,这种情况就是出现了非原子性;因为每个CPU都有只属于自己的一块工作内存,其他CPU无法访问,在某种情况下就会出现数据不一致;如:现在有两个CPU都从共享的工作内存空间读取到num=0,此时CPU1将num数值改为1,此时CPU2也将num数值改为1,现在两个CPU都需要把num的值写会到内存中,但是CPU1先进行写入内存操作,之后CPU2才执行,此时的实际结果是num的值是1;但是我们需要的结果是num=2;这与我们期望结果不一致;这也就是导致的第二个问题,不可见性;由于CPU执行的时候会将指令的顺序进行重新排序(编译优化),所以CPU实际执行的指令顺序与我们的代码顺序可能是不一致的,但是有一些业务逻辑我们是不希望让这种不一致出现的;这也就导致了第三个问题,非有序性

我们的并发编程就是围绕着解决这三个问题去展开的

从计算数据结果去分析,其实是因为:在解决CPU,内存,磁盘之间读写速度差异大,提高计算机效率的时候,引入了缓存,线程,并发,并行的思想之后带来了新问题,不管是非原子性,不可见性,还是非有序性,它们三者最终导致的问题都是程序对内存中数据操作结果达不到预期目标


二、volatile关键字

共享变量被volatile修饰后,保证不同线程对这个变量的操作互相之间可以看见;保证修饰的变量编译优化的时候不会被重排序;volatile保证了有序性和可见性,但是无法保证原子性

三、解决非原子性

可以通过原子变量与加锁的方式去解决非原子性的操作

1.原子变量

对于count++ 这种操作来说,在计算机底层实现的时候,一共分了三步,第一步从主内存读取操作数据到工作内存,第二步操作工作内存,对操作数据进行改变,第三步将操作完成的数据写回到主内存。这三步实现的任何一个时刻都有可能会发生并发,或者阻塞,让其不具备原子性(一个操作是不可打破的,它的所有操作是一个整体操作,缺一不可,它一旦执行后,不应该被打破);此时我们需要解决这个非原子性问题;使用synchronized 成本太高了,需要先获取锁,最后需要释放锁,获取不到锁的情况下需要等待,还会有线程的上下文切换,这些操作成本太大,不值得
对于以上这种情况,完全可以使用原子变量代替

在java.util.current包里面包含一些帮助我们实现原子性的工具类如:
AtomicBoolean: 原子Boolean 类型,常用来程序中表示一个标志位
AtomicInteger: 原子Integer 类型

这些原子变量的底层是通过volatile关键字和CAS(compare and swap)机制实现的;比较并交换

CAS机制:比较并交换机制;主要思想是一个线程从内存中拿到了数据,这个数据我们记作内存值V,我们对这个V进行操作之后,得到一个更新值B,此时我们线程需要把自己工作内存中的这个更新值B写入到主内存中,此时会再次从主内存获取这个内存值,此时从主内存获取到的值记作期望值A,我们比较期望值A与内存值V,如果我们发现A==V,则说明我们可以操作,现在将更新值B写入到主内存中;如果发现A与V不相等,我们则会进行循环操作,重新去读取内存值,操作数据得到更新值,然后去比较内存值与期望值,直到我们成功将更新值写入到内存

从描述中我们可以看出来,CAS不适合并发量大场景,因为这种循环是很浪费空间的,一旦大量进程都进入循环,那计算机资源消耗还是很大的

ABA问题:CAS机制只是对比了期望值与内存值的数值是否一致,这个样子不严谨,如主内存中这个值是1,线程1读入1,更改为2;此时线程1阻塞,线程2读入1,更改为2,也写入到主内存,成功将主内存值改为2;线程3进入,读入2,更改为1,也写入到主内存,成功将主内存值改回1;此时线程1得到执行权,它去比较发现可以执行,会将2写入到主内存。但是实际情况我们内存的这个值是被更改,最后又改回去的;CAS算法对这种情况并不可以检测到

解决方法是,我们加入版本号,每对值进行一次更改操作,我们就将版本更新,最后在比较期望值与内存值的基础上加入比较版本号

2.锁

不全是真正的锁,有的是锁思想,锁特性,锁设计,有的是锁状态
乐观锁: 乐观的认为,不需要去使用锁可以实现并发安全,使用CAS思想去不断尝试
悲观锁: 每次访问数据的时候,悲观的认为数据都有可能出现不一致,使用锁机制对并发进行处理,使每次都只会有一个进程访问数据
可重入锁: 也称之为递归锁,比如synchronized和ReentrantLock都是可重入锁,有效避免了死锁;当一个线程进入外层方法获取锁,内层调用了另一个需要获取锁的方法是可以进入的
读写锁: 这是一种真正的锁,维护了两个锁,一个是读锁,一个是写锁;读取数据的时候,允许多个进程进入;写入数据的时候,只允许一个线程进入
分段锁: 这是一种锁的思想,对代码进行分段加锁,一次就可以让多个线程进入;锁的粒度更小,提高并发
自旋锁: 采用CAS思想;不断地尝试获取锁,如果没有锁则进入,有锁则继续尝试获取锁,不会让线程阻塞,但是消耗CPU资源;适合用于加锁时间短的场景
共享锁: 如读锁;这个锁可以被多个线程获取
独占锁: 也叫互斥锁,比如synchronized和ReentrantLock都是独占锁,一次只允许让一个线程进入
公平锁: 按照请求锁的顺序,去分配锁;如ReentrantLock可以通过AQS将其改为公平锁
非公平锁: 不按照请求顺序去分配锁;比如synchronized和ReentrantLock都是非公平锁

synchronized中,对锁进行了优化,提出了有四种锁状态,去优化锁的释放与获取效率:

  1. 无锁状态: 该对象没有锁
  2. 偏向锁: 如果这个对象加了锁的,并且只有一个线程访问这个对象,此时会将线程ID存入到对象头中,下次这个线程再次访问的时候就会立马分配锁给这个线程
  3. 轻量级锁: 当其他线程也访问这个对象,则将这个锁升级为轻量级锁;不会让线程进入阻塞状态,通过自旋的思想去获取锁,提高效率
  4. 重量级锁: 线程访问数量过多,线程自旋数量达到一定数量,锁升级为重量级锁,线程进入阻塞状态,让操作系统进行线程调度

JVM为了提高锁的释放与获取效率,专门为synchronized设计的锁状态

3.对象头

实例对象包括:

  1. 对象头: 包括记录GC分代年龄,偏向线程ID,线程持有的锁,锁状态标志等
  2. 实例数据 : 就是对象的属性数据
  3. 对象对齐填充: 虚拟机自动内存管理系统要求对象起始地址必须是8字节的整数倍;换句话说就是任何对象的大小都必须是8字节的整数倍;对象头已经被精心设计为8字节的1倍或者2倍,当我们的实例数据的大小不是8字节的整数倍的时候,就需要使用对象对齐填充部分补齐

4.synchronized的底层实现

synchronized是Java中的关键字,可以修饰代码块与方法;一次只允许一个线程进入,当有一个线程进入后,在对象头里面的锁标志就会+1(默认为0),当线程执行完毕,锁标志就会-1;
Java提供了原子性内置锁,Java每个对象都可以作为监视器对象;Java的synchronized实现就是基于进入和退出监视器对象实现的;如:被synchronized修饰的方法执行后,会被ACC_SYNCHRONIZED标记,之后会被监视器对象监视,也就是monitorenter,此时其他线程就不会进入,当同步方法执行完毕,monitorexit退出监视器对象

5.AQS

AbstractQueuedSynchronized,抽象的同步队列;使用AQS可以高效构建锁和同步器的框架;是JUC中核心的API;

实现原理: 在内部存在一个用volatile修饰的state变量,初始化为0,在多线程条件下,当有一个线程进入共享资源后,会将这个state值+1,此时共享资源成了临界资源,当有其他线程访问共享资源的时候,发现state状态不为0,则会到一个FIFO队列中等待,当state的值为0的时候,会从这个等待队列中让下一个节点对象获取state。另外state是原子变量,使用volatile修饰,保证它的有序性和可见性,并且使用原子操作方法,保证线程安全;这个等待队列是双向链表实现的,head节点代表当前占用的线程,其他节点依次是由于暂时获取不到锁,依次排队等待的线程;队列由Node对象组成,Node是AQS中的内部类trant

6.ReentrantLock

可重入锁,ReentrantLock类里面包含三个内部类(NonfairSync,FairSync,Sync);ReentrantLock默认是非公平锁,可以通过构造器或者AQS去将其改变为公平锁;NonFairSync和FairSync两者都继承Sync的内部类;Sync继承了AbstractQueuedSynchronizer;

四、常用JUC类

1.ConcurrentHashMap

这是Java中提供的支持高并发的哈希表;JDK5的时候添加ConcurrentHashMap内部使用分段锁让线程安全;到了JDK8之后,放弃了用分段锁,因为对于哈希表来讲,加入分段锁浪费空间;并且线程在哈希表中争抢同一个锁的概率十分小;分段锁反而让更新操作长时间等待;JDK8之后是在Node上使用CAS和synchronized的方式保证原子性;当添加元素到哈希表时会判断存储数据的节点是否为null,如果是null,则使用CAS机制直接添加数据到节点;如果判断结果不是null,则将第一个节点作为锁对象,实现加锁,阻止其他线程访问;

2.CopyOnWriteArrayList

这是Java中提供支持高并发的单列数据容器;它支持读数据可以多线进行,写数据的时候只能单线程进行;在CopyOnWriteArrayList内部写入数据的时候先将原数据进行拷贝,得到副本,将修改后的数据写入到副本中,最后再用数据副本替换原来的数据;这样子实现了读数据与写数据互不影响;

3.CountDownLatch

如果此时我们需要在线程1与线程2执行完毕之后再执行线程3;那么此时可以使用CountDownLatch

4.CyclicBarrier

有若干个线程,比如说有五个线程,需要它们都到达了某一个点之后才能开始一起执行,也就是说假如其中只有四个线程到达了这个点,还差一个线程没到达,此时这四个线程都会进入等待状态,直到第五个线程也到达了这个点之后,这五个线程才开始一起进行执行状态

当没有可选的Runnable时
当所有线程到达屏障时,不需要进行汇总,最后一个线程到达时,屏障消除,所有线程继续执行

当有可选的Runnable时
当所有线程到达屏障时,需要进行汇总操作,等汇总操作进行完,屏障消除,所有线程继续执行

Java中的并发编程相关推荐

  1. 《Java并发编程的艺术》——Java中的并发工具类、线程池、Execute框架(笔记)

    文章目录 八.Java中的并发工具类 8.1 等待多线程完成的CountDownLatch 8.2 同步屏障CyclicBarrier 8.2.1 CyclicBarrier简介 8.2.2 Cycl ...

  2. 《Java并发编程的艺术》读后笔记-Java中的并发工具类(第八章)

    文章目录 <Java并发编程的艺术>读后笔记-Java中的并发工具类(第八章) 1.等待多线程完成的CountDownLatch 2.同步屏障CyclicBarrier 2.1 Cycli ...

  3. Java中的多线程编程(超详细总结)

    文章目录 Java中的多线程编程(超详细总结) 一.线程与多线程的概念 二.线程与进程之间的关系 三.一个线程的生命周期 四.多线程的目的和意义 五.线程的实现的方式 Java中的多线程编程(超详细总 ...

  4. 《Java线程与并发编程实践》—— 2.3 谨防活跃性问题

    本节书摘来异步社区<Java线程与并发编程实践>一书中的第2章,第2.3节,作者: [美]Jeff Friesen,更多章节内容可以访问云栖社区"异步社区"公众号查看. ...

  5. Java增强之并发编程

    Java增强之并发编程 1 多线程 1.1 进程及线程 程序启动的时候,电脑会把这个程序加载到内存,在内存中需要给当前的程序分配一段的独立运行的空间,这个空间就专门负责这个程序的运行.每个应用程序运行 ...

  6. Java零基础并发编程入门

    Java零基础并发编程入门 并发编程主要包括: 线程,同步,future,锁,fork/join, volatile,信号量,cas(原子性,可见性,顺序一致性),临界性,分布式 了解基础: JMM: ...

  7. java书籍_还搞不定Java多线程和并发编程面试题?你可能需要这一份书单!

    点击蓝色"程序员书单"关注我哟 加个"星标",每天带你读好书! ​ 在介绍本书单之前,我想先问一下各位读者,你们之前对于Java并发编程的了解有多少呢.经过了1 ...

  8. 《Java线程与并发编程实践》—— 2.6 小结

    本节书摘来异步社区<Java线程与并发编程实践>一书中的第2章,第2.6节,作者: [美]Jeff Friesen,更多章节内容可以访问云栖社区"异步社区"公众号查看. ...

  9. 《Java线程与并发编程实践》—— 1.2 操作更高级的线程任务

    本节书摘来异步社区<Java线程与并发编程实践>一书中的第1章,第1.2节,作者: [美]Jeff Friesen,更多章节内容可以访问云栖社区"异步社区"公众号查看. ...

最新文章

  1. JZOJ 5878. 【NOIP2018提高组模拟9.22】电路图 A
  2. Android 从一个Activity跳转到另一个Activity获取第二个Activity的返回值
  3. [Python学习] 专题五.列表基础知识 二维list排序、获取下标和处理txt文本实例
  4. leetcode 424. 替换后的最长重复字符(滑动窗口)
  5. Hbase JMX 监控 - Region
  6. JAVA中日期格式SimpleDateFormat
  7. 《天天数学》连载03:一月三日
  8. 前台传到后台类型数组对象
  9. javascript节点的操作 创建、添加、移除、移动、复制
  10. C/C++指针和取地址操作
  11. restlet java_restlet(javase版本) 的最基本使用
  12. 当代移动通信发展四个阶段
  13. vue组件库,插件大全
  14. 《仿人机器人原理与实战》一第2章
  15. 【iOS】通过URL Scheme启动app(收集了常用的app的URL Scheme)
  16. SharePoint 2016 Search 定制开发简介系列七-Search Database with Security Trimming
  17. 用Scrapy框架爬取豆瓣电影,构建豆瓣电影预测评分模型
  18. 览器野史 UserAgent列传
  19. 应用程序正常初始化(0xc0000005)失败
  20. 成都艾司博讯电商:拼多多店铺类目选错如何退店?

热门文章

  1. linux关于ssh免密登录、known_hosts文件
  2. Acwing 969. 志愿者招募
  3. 水之城 Aquatico V1.009.0 官方中文 免安装【4.04G】
  4. 如何有效地报告 Bug
  5. 软件工程基础第一次作业:阅读与准备作业
  6. 小猫钓鱼——(c++用栈和队列实现)
  7. [涨停战法] 以涨停板为信号抓短线爆发力牛股
  8. 我的世界服务器伤害增加bug,我的世界惊现全新无限刷物品bug 服主大大都要注意了...
  9. 我比较欣赏的歌词集锦
  10. 高层建筑结构健康监测:智能监测与预警守护建筑安全