1.JMM(Java 内存模型)
- JMM 是围绕原子性、可见性、有序性建立的一种规范,它的目的是屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台下都能达到一致的内存访问效果。
- 变量都存放在主内存中,每条线程有自己独立的工作内存,主内存的副本保存在工作内存中,线程对变量的读写操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程也无法直接访问其他工作内存中的数据。线程间变量值的传递通过主内存来完成。
- 原子性是指一个操作不能被中断。
- 可见性是指一个线程修改了某一个共享变量的值时,其他线程能立即知道这个修改。
- 有序性:程序执行时可能进行指令重排,重排后的指令与原指令的顺序未必一致。
2. volatile
- volatile不能保证原子性。
- volatile可以保证可见性,当一个线程修改了volatile修饰变量的值时,该值会被写入主内存,而其他CPU的缓存区中存储的该变量的值也会因此而失效,等到需要的时候再重新从主内存中获取该变量最新的值。
- volatile可以保证有序性,它禁止了指令重排序,使得代码按照我们所期望的顺序来执行。
- 在实践中,volatile一个重要作用是与CAS结合,比如java.util.concurrent.atomic 包下的AtomicInteger,它通过volatile来修饰value,来保证获得的是内存中的最新值。还有双重校验锁的单例模式用Volatile修饰静态变量,禁止指令重排。
3. synchronized
作用、原理:
- synchronized可以保证原子性,它对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证了线程间的安全。
- 可以保证可见性,当线程获得锁时会清空工作内存,从主内存更新最新数据,当释放锁时会将更新同步到主内存中。
- 可以保证有序性,由于synchronized限制每次只有一个线程可以访问同步块,因此无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的,换句话说就是被关键字synchronized限制的多个线程是串行执行的,有序性问题自然得到了解决。
- 原理: 使用了两个监控指令 enter 和 exit,enter 指令指向代码块开始位置,执行它时线程会尝试获取锁,计数器为 0 时可以成功获取,获取后计数器设为 1。exit 指令用于释放锁,成功释放锁后计数器设回 0 。
synchronized修饰位置 / 用法:
- 修饰实例方法,给实例对象加锁。
- 修饰静态方法,给类对象加锁。
- 修饰同步代码块,给指定对象加锁。
4. Volatile、Synchronized两者的区别联系
- volatile本质是告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile只能修饰变量,synchronized可以修饰变量,方法及代码块。
- synchronized是独占锁,会造成线程的阻塞,存在线程上下文切换和线程重新调度的开销;而volatile不需要加锁,不会造成线程的阻塞,没有上下文切换的开销,相比synchronized 更轻量级。但JDK1.6 对 Synchronized 进行了各种优化,性能显著提升。
- volatile能保证数据的可见性,不能保证原子性;而synchronized既能保证可见性又能保证原子性。
- volatile禁止了指令重排序,它标记的变量不会被编译器优化。
5. ReentrantLock 与 Synchronized 的区别
- 两者都是可重入锁,也就是自己可以再次获取自己内部的锁,每次获取后,锁的计数器自增1。
- ReentrantLock 是JDK实现的,Synchronized 是JVM实现的。
- JDK1.6对Synchronized做了很多优化,比如偏向锁、轻量级锁、自旋锁等,优化后它们两者性能大致相同。锁优化
- ReentrantLock 比 synchronized 增加了一些高级功能:
- 等待可中断: 正在等待的线程可以放弃等待,改为处理其他事情。而 synchronized 不行。
- ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。公平锁指的是先等待的线程先获得锁,能够减少饥饿发生的概率,因为等待越久的请求越优先满足。非公平锁指的是无视队列顺序直接抢锁,可能导致某些线程一直拿不到锁,造成线程饥饿。公平锁和非公平锁
- 可实现选择性通知: Synchronized 与 wait 、notify 和 notifyAll 方法结合使用可以实现等待通知机制,但不够灵活,notify 被通知的线程由JVM选择,notifyAll 会通知所有等待状态的线程,存在效率问题。而 ReentrantLock 通过与 Condition 接口和 newCondition 方法结合使用,使线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在线程调度上更加灵活。
CAS
- 使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文切换和重新调度的开销。java提供了非阻塞的volatile关键字,在一定程度上弥补了锁带来的开销问题,但它只能保证共享变量的可见性,不能解决读、改、写 等的原子性问题。于是有了CAS,compare and swap ,非阻塞原子性操作。
- CAS原理:java.util.concurrent.atomic原子包下的类都是采用CAS来实现的无锁,比如AtomicInteger,里面有一个自增方法incrementAndGet,它表示一个无限循环,也就是CAS的自旋,循环体当中做了三件事:1.获取当前值。2.当前值+1,计算出目标值。3.进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤,这里通过volatile关键字来保证获取的是内存中的最新值。CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B,当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作。
- CAS优点:由于其非阻塞性,它对死锁问题天生免疫,并且线程间的相互影响也比基于锁的方式要小;使用CAS这种无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此它比基于锁的方式拥有更好的性能。
- CAS的问题:
①ABA问题:因为CAS需要检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路是使用版本号。在数据前面追加上版本号,每次更新数据时把版本号加1,那么A→B→A就会变成1A→2B→3A。提交更新时,如果第一次读取的版本号与最后更新的版本号相等,才更新成功。
②循环时间长,CPU开销大。在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
③只能保证一个变量的原子性操作。如果对多个共享变量进行操作,那么CAS无法保证操作的原子性,这时需要用synchronized来解决。 - Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
- CAS应用场景:CAS 适合读多写少、冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;java.util.concurrent.atomic原子包下的类都是采用CAS来实现的无锁(AtomicInteger);在高并发环境下,对同一个数据的并发读与并发写容易导致数据的一致性问题,这里也可以采取CAS机制,在进行更新操作时,比较一下初始值,如果初始值变了,则更新失败。