java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据,操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
程序次序规则:一个线程内写在前面的操作先行发生于后面的。
锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。这个修饰的变量会禁止了指令的重排序
线程启动规则:线程的 start 方法先行发生于线程的每个动作。 和线程的终止规则相反。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终止规则:线程中所有操作先行发生于对线程的终止检测。
对象终结规则:对象的初始化先行发生于 finalize 方法。
传递性规则:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能保证可见性。
根据MESI缓存一致性协议,因为volatile修饰的变量再修改之后会写到主内存,在通过总线的时候,会被其它线程检测到,从而修改自己主内存副本失效,再读的时候,可从主内存读。
即虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。
保证变量对所有线程的可见性
。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。禁止指令重排序优化
。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。关于Callable接口实现多线程的方法
与使用Runnable相比,Callable功能更强大:
相比于run()方法,call()方法可以有返回值
call()方法可以抛出异常,被外面的操作捕获,获取异常的信息
Callable支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
Future接口:
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutureTask是Future接口的唯一的实现类。
FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。
实现多线程的过程如下:
创建一个实现Callable的实现类。
实现call()方法,将此线程需要执行的操作声明在call()中。
创建Callable接口实现类的对象。
将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象。
将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
线程状态有New, RUNNABLE, BLOCK, WAITING, TIMED_WAITING, THERMINATED NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。
WAITING: 等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。
TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long) Object.wait(long) Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED:结束状态。线程调用完run方法进入该状态。
没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后执行接下来任务,复用已创建的线程,降低开销、控制最大并发数。
线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。
将任务派发给线程池时,会出现以下几种情况
Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。产生线程池的函数ThreadPoolExecutor也是Executor的具体实现类。
阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。
存在的问题
对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。
Java 有个类叫 Unsafe
类,这个类类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。
对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。
到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。
乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。
CAS 算法的思路如下:
CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。 基本数据类型的原子类有:
Atomic数组类型有:
Atomic引用类型有
FieldUpdater类型:
以AtomicIntger 为例: 方法getAndIncrement:以原子方式将当前的值加1,具体实现为:
总结来说:就是不断的循环,知道修改成功。
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。 只能一次性使用,不能reset。
CyclicBarrier 主要功能和countDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。
Semaphore即信号量。 Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。
Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。线程通过exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
JDK7采用锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。
get 除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。
put 须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。
JDK8的改进
Java 对象底层都关联一个的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。
synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor,这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。
执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。
JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。
轻量级锁一般是用用户态线程,线程之间的切换不需要经过操作系统,而重量级锁是指线程之间的切换需要操作系统。
线程:内核态 纤程:用户态
申请流程:
即自适应自旋、锁消除、锁粗化、锁升级等策略偏。
线程获取锁失败后,可以采用这样的策略,可以不放弃 CPU ,不停的重试内重试,这种操作也称为自旋锁。
自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。
锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。
锁消除是一种更为彻底的优化,在编译时,java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。
Lock 接是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。
AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。 AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。 AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改。
获取:(acquire)
释放:(release)
获取锁(acquireShared)
释放(releaseShared)
保证只有一个线程可以获取对象的锁,并执行代码块,其他线程不能在该线程执行代码块时执行。
三色标记法是垃圾收集器CMS和G1使用的标记方法,该方法把对象分为三种颜色:
因此,对三色标记法来说,所有对象都可以看作由白色集合,黑色集合,灰色集合组成。。通过这种标记方法的访问过程如下:
对于数据库中,每个线程通过threadlocal维护自己和该线程对应客户端对于数据库的连接。
为了保证当使用完成后,该threadlocal可以被JVM回收,不会产生内存泄漏的情况。
Mark word是对象头中的一部分,其中存储了对象的哈希码,GC分代年龄,锁状态标志,偏向线程ID等信息。
Java多线程的新建对应操作系统的新建状态。
Java多线程的Runnable状态对应操作系统的就绪和运行状态。
Java多线程的阻塞,等待和限时等待,对应操作系统的阻塞状态。
Java多线程的终止状态,对应操作系统的终止状态。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。