Java并发,学习资料
Java并发,知识点概述
Java并发,基础
并行和并发】
并行是指“并排行走”或“同时实行或实施”。在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。
为什么要让计算机同时处理多项任务】
因为计算机运算速度很快,但需要的数据往往都需要涉及:磁盘I/O、网络通信、数据库访问,这些速度很慢,为了避免处理器在大部分时间里都处于等待其他资源的空闲状态,就要让它先处理别的,即多任务处理。
笔者强烈建议多使用JDK并发包提供的并发容器和工具类来解决并发 问题,因为这些类都已经通过了充分的测试和优化。
Java语言和虚拟机提供了许多工具,把并发编程的门槛降低了不少。各 种中间件服务器、各类框架也都努力地替程序员隐藏尽可能多的线程并发细节,使得程序员在编码时 能更关注业务逻辑,而不是花费大部分时间去关注此服务会同时被多少人调用、如何处理数据争用、 协调硬件资源。但是无论语言、中间件和框架再如何先进,开发人员都不应期望它们能独立完成所有 并发处理的事情,了解并发的内幕仍然是成为一个高级程序员不可缺少的课程。
Java并发的底层机制原理
处理器如何实现原子操作----2022.3.6:不懂
32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写 入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节 的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位 的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂 内存操作的原子性。
(1)使用总线锁保证原子性
想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享 变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 处理器可以独占共享内存
使用总线锁保证原子性,存在的缺点】
在同一时刻,我们只需保证对某个内存地址 的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处 理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。可以使用缓存锁定代替总线锁定来进行优化。
(2)使用缓存锁保证原子性
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在 处理器内部缓存中进行,并不需要声明总线锁。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声 言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子 性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处 理器回写已被锁定的缓存行的数据时,会使缓存行无效,在如图2-3所示的例子中,当CPU1修 改缓存行中的i时使用了缓存锁定,那么CPU2就不能同时缓存i的缓存行。
处理器不会使用缓存锁定的两种情况】
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。
第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定
针对以上两个机制,我们通过Intel处理器提供了很多Lock前缀的指令来实现。例如,位测 试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其他一些操作数和逻辑指 令(如ADD、OR)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
Java如何实现原子操作—2022.3.6:不懂
在Java中可以通过循环CAS和锁的方式来实现原子操作。
(1)使用循环CAS实现原子操作
JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子 方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更 新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和 自减1。
CAS实现原子操作的三大问题】1)ABA问题。2)循环时间长开销大。3)只能保证一个共享变量的原子操作。
2)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁 机制,有偏向锁、轻量级锁和互斥锁。除了偏向锁,JVM实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时 候使用循环CAS释放锁。
Java内存模型JMM和并发
Java的并发采用的是共享内存模型】
并发编程中需要处理的的两个关键问题,线程之间如何通信及线程之间如何同步(这里的 线程是指并发执行的活动实体)。Java的并发中线程之间的通信是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对 程序员完全透明。编写多线程程序的Java程序员需要理解隐式进行的线程之间通信的工作 机制,以解决可能会遇到各种奇怪的内存可见性问题。
Java内存模型JMM】
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享 变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽 象关系:
线程之间的共享变量存储在主内存(Main Memory)中。每个线程都有一个私有的本地 内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的 一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优 化。
从图3-1来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。 1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。 2)线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图(见图3-2)来说明这两个步骤。
如图3-2所示,本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个 内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存 A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内 存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时 线程B的本地内存的x值也变为了1。 从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要 经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供 内存可见性保证。
先行发生happens-before原则
先行发生happens-before原则,概述&&作用】如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变 得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点,这是因为Java语言中有一 个“先行发生”(Happens-Before)的原则。happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存 可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。可用来判断数据是否存在竞争,线程是 否安全;可以通过几条简单规则一揽子解决并发环境下两个操 作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的定义之中。
先行发生Happens-Before原则,解释】
先行发生是Java内存模型中定义的两项操作之间的偏 序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一 个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关 系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。一个happens-before规则对应于一个或多个编译器和处理器重排序规则。
与程序员密切相关的happens-before规则如下。
·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 ·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
Java语言无须任何同步手段保障就能成立的先行发生规则】
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已 经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出 来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
·程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行 发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循 环等结构。
·管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
·volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量 的读操作,这里的“后面”同样是指时间上的先后。
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
·线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检 测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止 执行。
·线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
·对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
·传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出 操作A先行发生于操作C的结论。
如何使用这些规则去判定操作间是否具备顺序性】----见书《深入理解JVM》12.3.6
下面演示一下如何 使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全。
操作之间的数据依赖性
操作之间的数据依赖性,概述】如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间 就存在数据依赖性。
数据依赖的类型:写后读、写后写、读后写。
重排序,概述】
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵 守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
其他
并发编程的上下文切换】
当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。这是因为线程有创建和上下文切换的开销。
如何减少上下文切换】
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
·CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
·使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这 样会造成大量线程都处于等待状态。
·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
并发编程的资源限制】
在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。硬件资源限 制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接 数和socket连接数等。
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让 程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同 的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这 笔数据。 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket 连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
资源限制,可能带来的问题】
将某段串行的代码并发执行,但由于受限于资源,仍然在串行执行,这时候程序不仅不 会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
如何在资源限制情况下进行并发编程】
根据不同的资源限制调整 程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作 时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则 某些线程会被阻塞,等待数据库连接。
Java中所使用的并发机制依赖于JVM的实现和 CPU的指令。
Java-----线程状态
Java线程的状态,概述】Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并 且可以通过特定的方法在不同状态之间转换。New (新创建)•Runnable (可运行)•Blocked (被阻塞)•Waiting (等待)•Timed waiting (计时等待)•Terminated (被终止)
New (新创建)】创建后尚未启动的线程处于这种状态。当用 new 操作符创建一个新线程时,如 new Thread®, 该线程还没有开始运行。这意味着它的状态是 new。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。
•Runnable (可运行)】包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可 能正在执行,也有可能正在等待着操作系统为它分配执行时间。一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线桿可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。注:在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行。一旦一个线程开始运行,它不必始终保持运行,运行中的线程可能会被中断以给其他线程获得运行机会。
•Blocked (被阻塞)】当一个线程试图获取一个内部的对象锁(而不是 javiutiUoncurrent 库中的锁,) 而该锁被其他线程持有, 则该线程进人阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
•Waiting (等待)】处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线 程显式唤醒。当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
•Timed waiting (计时等待)】处于这种状态的线程也不会被分配处理器执行时间,不过无须等待 被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。有几个方法有一个超时参数。调用它们导致线程进人计时等待( timed waiting ) 状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep 和 Object.wait、Thread.join、 Lock.tryLock 以及 Condition.await 的计时版。
•Terminated (被终止)】已终止线程的线程状态,线程已经结束执行。线程因如下两个原因之一而被终止:因为 run()方法正常退出而自然死亡。因为一个没有捕获的异常终止了 run() 方法而意外死亡。
“阻塞状态”与“等待状态”的区别】是“阻塞状态”在等待着获取到 一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时 间,或者唤醒动作的发生
守护线程
t.setDaemon(true);将线程转换为守护线程(daemon thread。
Java中的守护线程daemon线程知识点:
守护线程的唯一用途是为其他线程提供服务,一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
守护线程会在任何时候甚至在一个操作的中间发生中断所以守护线程应该永远不去访问固有资源, 如文件、 数据库
线程优先级】
在 Java 程序设计语言中,每一个线程有一个优先级。
默认情况下, 一个线程继承它的父线程的优先级。每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。
可以用 setPriority 方法提高或降低任何一个线程的优先级。
可以将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 ) 与 MAX_PRIORITY (定义为 10 ) 之间的任何值。NORM_PRIORITY 被定义为 5。
如果确实要使用优先级, 应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态, 低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择, 尽管这样会使低优先级的线程完全饿死。
构造线程
构造一个线程对象,线程对象在构造的时候需要提供线程所需要 的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程 继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对 象就初始化好了,在堆内存中等待着运行。
线程中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行 了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否 被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该 线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返 回false。
从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位 清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互 方式,而这种交互方式最适合用来取消或停止任务。
同步,相关
什么时候需要使用同步】
“ 如果向一个变量写入值, 而这个变量接下来可能会被另一个线程读取, 或者,从一个变量读值, 而这个变量可能是之前被另一个线程写入的, 此时必须使用同步”。--------------Brian Goetz
开发时如何选择】
使用优先级排序:1.最好使用java.util.concurrent 包,2.如果 synchronized 关键字适合你的程序 3.如果特别需要 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition。
Condition条件对象(又叫条件变量)】
线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象也叫条件变量( conditional variable )。一个锁对象可以有一个或多个相关的条件对象。可以用 newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
Condition对象调用 await方法&调用signalAll 方法】
一旦一个线程调用 await方法, 它进人该条件的等待集,当锁可用时,该线程不能马上解除阻塞,相反,它处于阻塞
状态,直到另一个线程调用同一条件上的 signalAll 方法时为止,这一调用会重新激活因为这一条件而等待的所有线程,当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们,同时, 它们将试图重新进人该对象,一旦锁成为可用的,它们中的某个将从 await 调用返回, 获得该锁并从被阻塞的地方继续执行,此时, 线程应该再次测试该条件, 由于无法确保该条件被满足—signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件, 值得再次去检测该条件。
通常, 对 await 的调用应该在如下形式的循环体中(自查)
当一个线程调用 await 时,它没有办法重新激活自身,它寄希望于其他线程调用 signalAll 方法,如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock) 现象,如果所有其他线程被阻塞, 最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法, 那么它也被阻塞,没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
应该何时调用 signalAll 呢? 经验上讲, 在对象的状态有利于等待线程的方向改变时调用signalAll。调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
signal方法
signal方法,是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的
阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行, 那么它再次被阻塞。如果没有其他线程再次调用 signal, 那么系统就死锁了
当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 await、signalAll 或signal 方法。
把解锁操作括在 finally 子句之内】
把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则, 其他线程将永远阻塞。要留心临界区中的代码,不要因为异常的抛出而跳出临界区。如果在临界区代码结束之前抛出了异常,finally 子句将释放锁,但会使对象可能处于一种受损状态。
锁是可重入的】
不懂。资料)因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( holdcount) 来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。
Lock 和 Condition 对象】
•锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
•锁可以管理试图进入被保护代码段的线程。
•锁可以拥有一个或多个相关的条件对象。
•每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制
synchronized
synchronized,概述】
synchronized可以修饰:实例方法、静态方法、包装代码块。
从 1.0 版开始,Java中的每一个对象都有一个内部锁,并且该锁有一个内部条件,由锁来管理那些试图进入synchronized 方法的线程,由条件来管理那些调用 wait 的线程。如果一个方法用 synchronized关键字声明,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁。
将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁,也就是说当这个方法被调用时,类名.class对象的锁被锁住,因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
synchronized 的作用】它主要确保多个线程 在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性 和排他性。
对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则 是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一 个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个 线程获取到由synchronized所保护对象的监视器。任意线程对Object(Object由synchronized保护)的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新 尝试对监视器的获取。
synchronized修饰方法,方法内的代码就变成了原子操作。synchronized实例方法实际保护的是同一个对象的方法调用。即this, this对象有一个锁和一个等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待。synchronized保护的是对象而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。即,有两个方法分别被synchronized修饰,两个线程无法同时执行这两个方法,而是顺序执行。一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。synchronized保护的是对象,对实例方法,保护的是当前实例对象this,对静态方法,保护的是类对象,实例对象和类对象是两个对象。Java编译器和虚拟机可以不断优化synchronized的实现,比如自动分析synchronized的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。
Synchonized在JVM里的实现原理】
JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步,
方法同步和代码块同步的实现细节不一样,代码块同步是使用monitorenter 和monitorexit指令实现的,方法同步是使用另外一种方式实现的。
“synchronized关键字是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明 要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作 为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来 决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。根据《Java虚拟机规范》的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果 这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象 锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。”—《深入理解JVM》
内部锁和条件存在一些局限】
包括: •不能中断一个正在试图获得锁的线程。 •试图获得锁时不能设定超时。 •每个锁仅有单一的条件, 可能是不够的。
选用Lock 和 Condition 对象or synchronized修饰的同步方法?】
最好既不使用 Lock/Condition 也不使用 synchronized 关键字。在许多情况下你可以使用 java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。你会看到如何使用阻塞队列来同步完成一个共同任务的线程。还应当研究一下并行流。 •如果 synchronized 关键字适合你的程序, 那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。 •如果特别需要 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition。
Java中的每一个对象都有一个内部锁,内部对象锁只有一个相关条件,wait 方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
synchronized关键字可以修饰实例方法、静态方法。
当synchronized关键字修饰静态方法,如果调用这种方法,该方法获得相关的类对象的内部锁。
synchronized的实际执行过程原理】----看书
synchronized的作用】可重入性(BD):资料)它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( holdcount) 来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。保证内存可见性:在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据,此方法成本高,选用给变量加修饰符volatile的方法。
可重入性】
可重入性是指一条线程能够反复进入被它自己持有锁的同步块的特性,即锁关联的计数器,如果持 有锁的线程再次获得它,则将计数器的值加一,每次释放锁时计数器的值减一,当计数器的值为零 时,才能真正释放锁。
synchronized的局限性】它不能尝试获取锁,也不响应中断,还可能会死锁。不过,相比显式锁,synchronized简单易用。synchronized代码块内蕴含了锁机制。从执行成本的角度看,持有锁是一个重量级(Heavy-Weight)的操作,synchronized是Java语言中一个重量级的操作,有经验的程序员都只会在确实必要的情况下才使用这种 操作。
关于synchronized的直接推论】:
·被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块 也不会出现自己把自己锁死的情况。
·被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他 线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制 正在等待锁的线程中断等待或超时退出。
Java中的每一个对象都可以作为锁。具体表现 为以下3种形式。 ·对于普通同步方法,锁是当前实例对象。 ·对于静态同步方法,锁是当前类的Class对象。 ·对于同步方法块,锁是Synchonized括号里配置的对象。
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。
JDK 6中加入了大量针对 synchronized锁的优化措施
synchronized与ReentrantLock都可满足需要时优先使用synchronized的情况】
·1.synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉 synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐 synchronized。
·2.Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不 会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确 保即使出现异常,锁也能被自动释放。
volatile
volatile,概述】
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
只涉及一两个实例域,可以不用内部锁和Lock,而是使用volatile,即将域声明为volatile。
volatile 关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
volatile 变量不能提供原子性,即无法保证:对Volatile 变量操作的语句不会被中断。
volatile,它在多处理器开发中保证了共享变量的“可见性”,即当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile.
volatile是轻量级的 synchronized,它比synchronized的使用和执行成本更低
因为它不会引起线程上下文的切换和调度。
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要 从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问 的可见性。
举个例子,定义一个表示程序是否运行的成员变量boolean on=true,那么另一个线程可能 对它执行关闭动作(on=false),这里涉及多个线程对变量的访问,因此需要将其定义成为 volatile boolean on=true,这样其他线程对它进行改变时,可以让所有线程感知到变化,因为所 有对on变量的访问和修改都需要以共享内存为准。但是,过多地使用volatile是不必要的,因为 它会降低程序执行的效率。
volatile的定义与实现原理】-----看书。
Java线程之间的通信—等待/通知机制
Java内置的等待/通知机制】
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,为了保障以上过程而出现的等待/通知机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
调用wait()、notify()以 及notifyAll()时需要注意的细节】
1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的 等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait()方法返回时能够感知到通知线程对变量做出的修改。
java.lang.ThreadLocal
ThreadLocal基本介绍】
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构,这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。通过java.lang.ThreadLocal类来实现线程本地存储的功能。有时可能要避免共享变量,使用ThreadLocal 辅助类为各个线程提供各自的实例。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。从字面意思上看,ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争。
This gives us the ability to store data individually for the current thread and simply wrap it within a special type of object.The TheadLocal construct allows us to store data that will be accessible only by a specific thread.
ThreadLocal的使用】
ThreadLocal的原理】
每一个线程的Thread对象中都 有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线 程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个 ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对 中找回对应的本地线程变量。
ThreadLocal的内存泄漏问题】
ThreadLocal进行了处理,但仍有可能发生。
final
将域声明为final,也可以供多个线程安全地共享
”关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易被正确、完整地 理解,以至于许多程序员都习惯去避免使用它,遇到需要处理多线程数据竞争问题的时候一律使用 synchronized来进行同步。了解volatile变量的语义对后面理解多线程操作的其他特性很有意义,在本节 中我们将多花费一些篇幅介绍volatile到底意味着什么。“------《深入理解Java虚拟机》12.3.3
死锁
死锁,概述】举例,每一个线程都试图从这个账户中取出大于该账户余额的钱,有可能会因为每一个线程要等待更多的钱款存人而导致所有线程都被阻塞,这样的状态称为死锁(deadlock )。
比如t1拿到锁之后,因为一些异常情况没有释放锁 (死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
一旦出现死锁,通过dump线程查看 到底是哪个线程出现了问题
避免死锁的几个常见方法】
避免一个线程同时获取多个锁。
·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 ·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
当程序挂起时, 键入 CTRL+, 将得到一个所有线程的列表。每一个线程有一个栈踪迹, 告诉你线程被阻塞的位置。
使用读 / 写锁的必要步骤:
为什么弃用 stop 和 suspend 方法】----看书
书14.5.13 锁测试与超时–不懂
关键字陈列:trylock()、lock()、超时参数
其他
如果使用锁, 就不能使用带资源的 try 语句。首先, 解锁方法名不是 close。不过,即使将它重命名, 带资源的 try 语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁, 你可能想使用多个线程共享的那个变量(而不是新变量)。
若不使用同步,处理器和编译器上可能出现的错】
•多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
•编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变!
编译器被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令。
未捕获异常处理器—不懂
涉及知识点:Thread.UncaughtExceptionHandle、ThreadGroup
java.Iang.Thread,方法】
ThreadCRunnable target()—构造一个新线程, 用于调用给定目标的 run() 方法。
void start( )----------线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。
void run( )-------调用关联 Runnable 的 run 方法。
void interrupt()-----向线程发送中断请求。线程的中断状态将被设置为 true。如果目前该线程被一个 sleep调用阻塞,那么,InterruptedException 异常被抛出。
static boolean interrupted()-----------测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用—它将当前线程的中断状态重置为 false
boolean islnterrupted()----------测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
static Thread currentThread()--------返回代表当前执行线程的 Thread 对象。
v o i d j o i n( ) --------等待终止指定的线程。当前线程等待thread线程终止之后才 从thread.join()返回。
v o i d join (long millis) ---------等待指定的线程死亡或者经过指定的毫秒数。
T h r e a d.S t a t e g e t S t a t e ()得到这一线程的状态;NEW、RUNNABLE BLOCKED、 WAITING HMED_WAmNG或 TERMINATED 之一。
Java线程调度】
线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式 (Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。
锁优化技术
锁优化技术,概述】锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行 效率
自旋锁
自旋锁,概述】
由于共享数据的锁定状态只会持续很短的一段时间,为了 这段时间去挂起和恢复线程并不值得。现在绝大多数的个人电脑和服务器都是多路(核)处理器系 统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们 就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很 快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自 旋锁。
自旋等待不能代替阻塞,自 旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很 短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理 器资源,而不会做任何有价值的工作,这就会带来性能的浪费。
因此自旋等待的时间必须有一定的限 度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。
自适应的自旋:自适应意味着自旋的时间不再是固定的了,而是由 前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚 刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成 功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自 旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资 源。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况 预测就会越来越精准,虚拟机就会变得越来越“聪明”了。
锁消除
"锁消除"技术,概述】锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享 数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可 以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
锁粗化
轻量级锁
轻量级锁,概述】
传统的锁机制使用操作系统互斥量来实现的锁被称为“重量级”锁,轻量级锁并不是 用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系 统互斥量产生的性能消耗。轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互 斥量。
HotSpot虚拟机的对象头是实现轻量级锁的关键。
轻量级锁的工作过程】-----见书13.3.4
偏向锁
偏向锁】
偏向锁也是JDK 6中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。
偏向锁是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏向锁会偏向于第一个获得它的线 程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需 要再进行同步。
偏向锁的原理和运作过程–见书13.3.5
HotSpot虚拟机的对象头是实现偏向锁的关键。
Java,线程安全
Java多线程情况下,数据安全程度细分类型及讨论】
为了更深入地理解线程安全,在这里我们可以不把线程安全当作一个非真即假的二元排他选项来 看待,而是按照线程安全的“安全程度”由强至弱来排序,可以将Java语言中各种操作共享的数 据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
-
1.不可变
在Java语言里面(特指JDK 5以后,即Java内存模型被修正之后的Java语言),不可变 (Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行 任何线程安全保障措施。
只要 一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都 不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接、 最纯粹的。Java语言中,如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰 它就可以保证它是不可变的。如果共享数据是一个对象,由于Java语言目前暂时还没有提供值类型的 支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行。
如果读者没想明白这句话所指 的意思,不妨类比java.lang.String类的对象实例,它是一个典型的不可变对象,用户调用它的 substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都 声明为final,这样在构造函数结束之后,它就是不可变的
在Java类库API中符合不可变要求的类型:String、枚举类型、java.lang.Number的部分子类:如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。 -
2.绝对线程安全
绝对的线程安全能够完全满足Brian Goetz给出的线程安全的定义,这个定义其实是很严格的,一 个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”可能需要付出非常高昂的, 甚至不切实际的代价。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。如何理解:绝对线程安全中的"绝对"的含义,看书《深入理解JVM》13.2.1,有一个测试Vector线程安全的测试。假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次 对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。 -
3.相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安 全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需 要在调用端使用额外的同步手段来保证调用的正确性。 在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。 -
4.线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对 象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类 库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。 -
5.线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害 的,应当尽量避免。一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对 象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同 步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就 肯定要产生死锁了。也正是这个原因,suspend()和resume()方法都已经被声明废弃了。常见的线程对立 的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
线程安全
”线程安全“,概述】
“线程安全”,定义】“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对 象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”----Brian Goetz
在许多场景中,我们会将以上定义中“调用这个对象的行为”限定为“单次调用”,这个定 义的其他描述能够成立的话,那么就已经可以称它是线程安全了。
Java语言,线程安全的实现方法
线程安全的实现,和虚拟机提供的同步和锁机制有很大关系。只要明白了Java虚拟机线程安全措施的原理与运作过程,自己再去思考代码如何编写就不是 一件困难的事情了。
Java中,如何实现线程安全】互斥同步(又叫阻塞同步)、非阻塞同步、无同步方案
互斥同步,概述】通过互斥的方式,以达到同步的目的。(同步,即多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用)。互斥 是因,同步是果;互斥是方法,同步是目的。互斥同步主要是考虑线程阻塞和唤醒,所以又叫阻塞同步(Blocking Synchronization)。互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢 复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。
非阻塞同步】
非阻塞同步,,概述】通俗地说 就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现 没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被 称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free) 编程。
乐观并发策略需要“硬件指令集的发展”,必须要求操作和冲突检测这 两个步骤具备原子性,只能靠硬件来保证原子性,硬件保证某些从语义上看起来需要多次操作的行为可以只通过一 条处理器指令就能完成,
这类指令常用的有:·测试并设置(Test-and-Set); ·获取并增加(Fetch-and-Increment); ·交换(Swap); ·比较并交换(Compare-and-Swap,下文称CAS); ·加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)。前面的三条是20世纪就已经存在于大多数指令集之中的处理器指令,后面的两条是现代处 理器新增的
CAS指令】
Java里最终暴露出来的是CAS操作,在JDK 5之后,Java类库中才开始使用CAS操作。该操作由sun.misc.Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。
CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V 表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合 A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的 旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
如何通过CAS操作避免阻塞同步】—《深入理解JVM》代码演示。
无同步方案
有一些代码天生就是线程安全的:可重入代码。线程本地存储。
-
可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何 时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不 会出现任何错误,也不会对结果有所影响。在特指多线程的上下文语境里(不涉及信号量等因 素[6]),我们可以认为可重入代码是线程安全代码的一个真子集,这意味着相对线程安全来说,可重 入性是更为基础的特性,它可以保证代码线程安全,即所有可重入的代码都是线程安全的,但并非所 有的线程安全的代码都是可重入的。
可重入代码有一些共同的特征,例如,不依赖全局变量、存储在堆上的数据和公用的系统资源, 用到的状态量都由参数中传入,不调用非可重入的方法等。我们可以通过一个比较简单的原则来判断 代码是否具备可重入性:如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返 回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。 -
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就 看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可 见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会 将产品的消费过程限制在一个线程中消费完,其中最重要的一种应用实例就是经典Web交互模型中 的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得 很多Web服务端应用都可以使用线程本地存储来解决线程安全问题。
java.util.concurrent包J.U.C
自JDK 5起 ,Java类库中新提供了java.util.concurrent包(可称为J.U.C包)
java.util.concurrent包.
java.util.concurrent.locks.Lock接口】
基于Lock接口,用户能够 以非块结构(Non-Block Structured)来实现互斥同步
BlockingQueue的原理和特点?--------------2021网易笔试题
java.util.concurrent 包提供了阻塞队列的几个变种。 默认情况下,LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数, 则那么等待了最长时间的线程会优先得到处理。通常,公平
性会降低性能,只有在确实非常需要时才使用它。PriorityBlockingQueue 是一个带优先级的队列, 而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的, 取元素的操作会阻
塞
重入锁ReentrantLock
重入锁(ReentrantLock)】重入锁(ReentrantLock)是Lock接口最常见的一种实现,是可 重入的。
重入锁的功能】:等待可中断、可实现公 平锁、锁可以绑定多个条件。
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改 为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平 锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非 公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平 锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized 中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一 个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用 newCondition()方法即可。
java.util.concurrent.locks 包 定 义 了 两 个 锁 类:ReentrantLock 类 和
ReentrantReadWriteLock 类。
ReentrantReadWriteLock 类
ReentrantReadWriteLock 类适用于:很多线程从一个数据结构读取数据而很少线程修改其中数据。允许对读者线程共享访问,写者线程依然必须是互斥访问的
阻塞队列
对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插人元素, 消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。例如,考虑银行转账程序, 转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。因此不需要同步。当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队列(blocking queue ) 导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。
工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢, 第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快, 它将等待第二个队列集赶上来。
阻塞队列的方法】
阻塞队列方法的分类】
阻塞队列方法分为以下 3类, 这取决于当队列满或空时它们的响应方式。如果将队列当作线程管理工具来使用, 将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列中移出元素时,add、 remove 和 element 操作抛出异常。当然,在一个多线程程序中, 队列会在任何时候空或满, 因此,一定要使用 offer、 poll 和 peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
线程安全的集合----dai不懂
《核心卷I》14.7:线程安全的集合----书,不懂。
阻塞队列属于线程安全的。
java.util.concurrent 包提供了映射、 有序集和队列的高效实现:ConcurrentHashMap、
ConcurrentSkipListMap 、 ConcurrentSkipListSet 和 ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。与大多数集合不同,size 方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。集合返回弱一致性( weakly consistent) 的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会拋出 ConcurrentModificationException 异常。
并发的散列映射表, 可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达 16 个写者线程同时执行。可以有更多的写者线程,但是, 如果同一时间多于 16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而, 恐怕没有这种必要。
CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数, 这样的安排是很有用的。当构建一个迭代器的时候, 它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组, 但是,集合的数组已经被替换了,因而,旧的迭代器拥有一致的(可能过时的)视图,访问它无须任何同步开销。
在 Java SE 8中, Arrays 类提供了大量并行化操作。静态 Arrays.parallelSort 方法可以对一个基本类型值或对象的数组排序。
从 Java 的初始版本开始,Vector 和 Hashtable 类就提供了线程安全的动态数组和散列表的实现,现在这些类被弃用了, 取而代之的是 AnayList 和 HashMap 类,这些类不是线程安全的,而集合库中提供了不同的机制。任何集合类都可以通过使用同步包装器(synchronization wrapper) 变成线程安全的:Col lections.synchronizedList( )、Col lections.synchronizedMap( ),结果集合的方法使用锁加以保护,提供了线程安全访问。
最好使用 java.Util.COnciirrent 包中定义的集合, 不使用同步包装器中的。
同步器—看书
其他
Thread.sleep()
Thread.sleep()的作用】
Thread.sleep causes the current thread to suspend execution for a specified period. This is an efficient means of making processor time available to the other threads of an application or other applications that might be running on a computer system. The sleep method can also be used for pacing and waiting for another thread with duties that are understood to have time requirements.
Thread.sleep(),其他】
不一定能睡眠预设的时长,可能会受到操作系统的限制和中断。
Thread.interrupted()
Thread类对象成员方法join()
The join method allows one thread to wait for the completion of another. If t is a Thread object whose thread is currently executing,t.join();causes the current thread to pause execution until t’s thread terminates. Overloads of join allow the programmer to specify a waiting period. However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.Like sleep, join responds to an interrupt by exiting with an InterruptedException.
Callable和Future-dai看书。
线程池thread pool 和执行器Executor类—看书
如果程序中创建了大量的生命期很短的线程,应该使用线程池( thread pool)。一个线程池中包含许多准备运行的空闲线程。将 Runnable 对象交给线程池, 就会有一个线程调用 run 方法。 当 run 方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。另一个使用线程池的理由是减少并发线程的数目,创建大量线程会大大降低性能甚至使虚拟机崩溃。
如果有一个会创建许多线程的算法, 应该使用一个线程数“ 固定的” 线程池以限制并发线程的总数。
执行器Executor类有许多静态工厂方法用来构建线程池
并发编程中需要考虑资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行, 但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不 会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。