本篇内容介绍了“Java并发编程的原理和应用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个线程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
使用多线程的原因主要有,更多的处理器核心、更快的响应时间、更好的编程模型(Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员可以更加专注问题的解决)。但是多线程编程仍然存在以下问题需要解决:线程安全问题、线程活性问题、上下文切换、可靠性等问题。
线程分配到的时间片决定了线程使用处理器资源的多少,线程优先级是决定线程需要多或者少分配一些处理器资源的线程属性。但是线程优先级不能作为程序程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
线程的状态主要有NEW(已创建未启动)、RUNNABLE(分为READY/RUNNING,前者表示可以被线程调度器调度,后者表示正在运行)、BLOCKED(阻塞I/O ,独占资源如锁,不会占用处理器资源)、WAITING(wait/join/park方法,notify/notifyAll/unpark方法恢复)、TIMED_WAITING(wait/join/sleep设定时间方法,类似WAITING,有限等待)、TERMINATED(结束态,正常返回/抛出异常提前终止)
图-Java线程的状态
如何进行线程的监视?主要途径是获取并查看程序的线程转储(Thread Dump),具体方式如下:
图-获取线程转储的方法
Daemon线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作,这意味着当一个虚拟机不存在非Daemon线程的时候,Java虚拟机将会推出。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
中断可以理解为线程的一个标识属性,并不是强迫终止一个线程而是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何及何时退出。对于以线程提供服务的程序模块而言,应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者(也可以通过设置boolean变量来控制是否停止任务并终止该线程),外部调用者应该调用这些方法而不是直接调用interrupt。
线程的暂停、恢复、停止操作suspend、resume、stop已经废弃。
Java内置的等待通知机制如下如:
图-等待/通知的相关方法
等待通知机制,是指一个线程A调用了对象O的wait方法进入等待状态,而另一个线程B调用了对象O的notify或者notifyAll方法,线程A收到通知后从对象O的wait方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait和notify/notifyAll的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
线程A执行threadB.join()语句的含义是:当前线程A等待threadB线程终止之后才能从threadB.join()返回,同时支持超时机制,如果线程threadB在给定的超时时间里没有终止,那么将会从该超时方法中直接返回。join底层实现借助等待通知机制,当线程终止时会调用线程自身的notifyAll方法,会通知所有等待在该线程对象上的线程。
开发在实际中经常碰到这样的调用场景:调用一个方法时等待一段时间,如果该方法能够在给定的时间段内得到结果,那么将立即返回,反之,超时将返回默认结果。
定义如下变量:等待持续时间,remaining = T,超时时间,future = now + T。实现的伪代码如下所示:
public synchronized Object get(long mills) throws InterruptedException { // 返回结果 Object result = new Object(); long future = System.currentTimeMillis() + mills; long remaining = mills; // 当超时大于0并且返回值不满足要求 while (result == null && remaining > 0) { wait(remaining); remaining = future - System.currentTimeMillis(); } return result; }
在并发编程中,需要解决两个关键问题:线程之间如何通信以及线程之间如何同步。通信是指线程之间以何种机制来交换信息,一般有两种:共享内存和消息传递。在共享内存的并发模型里,线程之间共享程序的公共状态,通过读-写内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,线程之间的通信总是隐式进行的,整个通信过程对于程序员完全透明。
Java中所有实例域、静态域和数组元素(共享变量)都在存储在堆内存中,堆内存在线程之间共享,局部变量,方法定义参数和异常处理参数不会在线程之间共享,因此不会有内存可见性问题,也不受内存模型的影响。Java线程之间的通信由Java内存模型(简称JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。如下图所示,线程A和线程B之间要通信的话,必须经历以下两个步骤:
线程A把本地内存A中更新过的共享变量刷新到主内存中;
线程B到主内存中区读取A之前已更新过的共享变量。
从整体上看,这两个步骤实际上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为程序提供内存可见性保证。
图-Java内存模型抽象示意图
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,重排序可能导致多线程程序出现内存可见性问题,JMM通过插入特定类型的内存屏障等方式,禁止特定类型的编译器重排序和处理器重排序,提供内存可见性保证。
图-从源码到最终执行的指令序列的示意图
图-内存屏障类型表
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,happens-before规则对应于一个或多个编译器和处理器重排序规则。两个操作之间具有happens-before关系并不意味着前一个操作必须要在后一个操作之前执行,仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。常见的规则有:监视器锁规则,对于一个锁的解锁,happens-before于随后对这个锁的加锁;volatile变量规则,对于volatile域的写,happens-before于后续对这个域的读;以及happens-before传递性等。
A happens-before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens-before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序。做到:在不改变程序执行结果的前提下,尽可能提供并行度。
as-if-serial语义是指,不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变,编译器和处理器不会对存在数据依赖关系(写后读、写后写、读后写)的操作做重排序,因为这种重排序会改变执行结果,但是如果不存在数据依赖关系,则可能被重排序。
在多线程中,对存在控制依赖的操作重排序,不会改变执行结果。在多线程程序中,对存在控制依赖的操作做重排序,可能会改变程序的执行结果。
顺序一致性内存模型,规定一个线程中的所有操作必须按照程序的顺序来执行;不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原子执行且立刻对所有线程可见。
关于重排序的案例可以参考:芋道源码-【死磕Java并发】—–Java内存模型之重排序
关于JMM,可以参考Hollis-再有人问你Java内存模型是什么,就把这篇文章发给他、Hollis-JVM内存结构VS Java内存模型 VS Java对象模型
芋道源码-Java各种锁的小结
匠心零度-面试官问:Java中的锁有哪些?我跪了
Lock接口实现锁功能,与synchronize类似,并且支持锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等synchronize关键字锁不具备的同步特性。
图-Lock接口主要特性
图-Lock API
队列同步器AQS,用来构建锁或者其它同步组件的基础框架 ,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器是实现锁(任意同步组件)的关键,锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
具体实现原理可以参考:大白话聊聊Java并发面试问题之谈谈你对AQS的理解?【石杉的架构笔记】
表示能够支持一个线程对资源的重复加锁。synchronized关键字隐式的支持重入锁,比如一个synchronized关键字修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次的获取该锁。ReentrantLock顾名思义也是支持可重复的。重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被阻塞,需要考虑以下两个问题:
线程再次获得锁,锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
锁的最终释放,线程重复n次获取锁之后,随后在第n次释放该锁后,其它线程能够获得到该锁,锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
公平与非公平(默认实现)获取锁的区别在于,是否需要等待比当前线程更早地请求获取锁的线程释放锁,非公平可能会出现“饥饿”问题,公平锁的实现代价是按照FIFO的原则进行大量的线程切换,需要针对公平性和吞吐量进行权衡。
分离读锁与写锁,使得并发性相对一般的排它锁有很大的提升,特别适合读多写少的场景(掘金-大白话聊聊Java并发面试问题之微服务注册中心的读写锁优化【石杉的架构笔记】)
public class Cache { // 线程非安全的map,通过读写锁保证线程安全 static Map<String, Object> map = new HashMap<>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } }
关于读写锁的内部原理可以参考:Java并发编程之锁机制之ReentrantReadWriteLock(读写锁)
构建同步组件的基础工具,提供最基本的线程阻塞和唤醒功能。
图-LockSupport提供的阻塞和唤醒方法
Condition是一种广义上的条件队列,为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。最典型的应用场景有生产者/消费者模式、ArrayBlockQueue等。
具体实现原理可以参考:匠心零度-死磕Java并发】—–J.U.C之Condition
对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步;对于同步代码块,JVM采用monitorenter、monitorexit两个指令来实现同步。synchronized可以保证原子性、可见性与有序性。
synchronized是重量级锁,Jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
深入分析可以参考:
深入理解多线程(一)——Synchronized的实现原理
深入理解多线程(二)—— Java的对象模型
深入理解多线程(三)—— Java的对象头
深入理解多线程(四)—— Moniter的实现原理
深入理解多线程(五)—— Java虚拟机的锁优化技术、InfoQ-聊聊并发(二)—Java SE1.6 中的 Synchronized(锁升级优化)
再有人问你synchronized是什么,就把这篇文章发给他。
轻量级synchronized,在多处理器开发中保证了共享变量的可见性,可见性指一个线程修改一个共享变量时,另外一个线程能读到这个修改的值,JMM确保所有线程看到这个变量的值是一致的。
深入分析可以参考:
深入理解Java中的volatile关键字
再有人问你volatile是什么,把这篇文章也发给他。
Java经典面试题:为什么 ConcurrentHashMap的读操作不需要加锁?
纯洁的微笑:面试必问之 ConcurrentHashMap 线程安全的具体实现方式
Hollis:详解ConcurrentHashMap及JDK8的优化
芋道源码:不止 JDK7 的 HashMap ,JDK8 的 ConcurrentHashMap 也会造成 CPU 100%?原因与解决~
程序猿DD: 解读Java 8 中为并发而生的 ConcurrentHashMap
CSDN-ConcurrentHashMap(JDK1.8)为什么要放弃Segment
基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,采用CAS算法实现。那么它是如何实现线程安全的,入队出队函数都是操作volatile变量:head、tail,所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法,对于poll也是这样的。
深入分析可以参考:
并发编程网-并发队列-无界非阻塞队列ConcurrentLinkedQueue原理探究
CSDN-Java并发编程之ConcurrentLinkedQueue详解
阻塞队列是支持阻塞插入和移除元素的队列,常用于生产者和消费者的场景。
JDK提供了7个阻塞队列:
ArrayBlockingQueue :数组、有界、FIFO、默认非公平 LinkedBlockingQueue :链表、有界、默认和最大长度Integer.MAX_VALUE PriorityBlockingQueue :支持优先级(排序规则)、无界 DelayQueue:支持延时、无界 SynchronousQueue:不存储元素、传递性场景,吞吐量高 LinkedTransferQueue:链表、无界、预占模式、ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集 LinkedBlockingDeque:链表、双向、容量可选
深入使用与分析可以参考:
程序猿DD-死磕Java并发:J.U.C之阻塞队列:LinkedTransferQueue
程序猿DD-死磕Java并发:J.U.C之阻塞队列:LinkedBlockingDeque
InfoQ-聊聊并发(七)——Java中的阻塞队列
阻塞队列的实现原理,使用通知模式实现,ArrayBlockingQueue借助notEmpty、notFull两个Condition来实现。当线程被阻塞队列阻塞时,线程会进入WAITING(parking)状态。
Fork/Join是切分并合并子任务的框架,主要步骤分为:分割出足够小的任务;执行任务并合并结果,子任务放在双端队列(工作窃取)里边,然后启动线程分别从双端队列里获取任务执行,子任务结果统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
深入分析和使用参考:
InfoQ-聊聊并发(八)—— Fork/Join框架介绍
包括原子更新基本类型AtomicBoolean、AtomicInteger、AtomicLong;原子更新数组类型AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;原子更新引用类型AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference;原子更新字段类型AtomicIntegeFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference(解决CAS的ABA问题)。
原子操作类对比直接加锁提供了一种更轻量级的原子性实现方案,采取乐观锁的思想,即冲突检测+数据更新,基于CAS实现(乐观锁是一种思想,CAS是这种思想的一种实现方式),CAS的做法很简单:即比较并替换,三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。底层实现,Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问,不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。
CAS相对于其它锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多,循环时间太长,cpu资源会消耗很高,换句话说CAS+自旋适合使用在竞争不激烈的低并发应用场景。在Java 8中引入了4个新的计数器类型,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator,主要思想是当竞争不激烈的时候所有线程都是通过CAS对同一个变量(Base)进行修改,当竞争激烈的时候会将根据当前线程哈希到对应Cell上进行修改(多段锁),主要原理是通过CAS乐观锁保证原子性,通过自旋保证当次修改的最终修改成功,通过降低锁粒度(多段锁)增加并发性能。
同时CAS只能保证一个共享变量原子操作,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位。
CAS只比对值,在一般场景下不会引起逻辑错误(例如余额),但是在特殊情况下,值虽然相同,但是可能已经是此A非彼A了(例如并发情况下的堆栈),因此CAS不能只比对值,还必须保证是原来的数据才能修改成功,一种做法是将值比对升级为版本号的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。(参考架构师之路-并发扣款一致性优化,CAS下ABA问题,这个话题还没聊完)。Java提供了AtomicStampedReference来解决,AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。
Java并发容器、框架、工具类
“Java并发编程的原理和应用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。