线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。(如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。)
java中与线程同步有关的关键字/类包括:
volatile、synchronized、Lock、AtomicInteger等concurrent包下的原子类。。。等
线程安全
概述:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
注:这意味着如若要实现线程安全,代码本身必须要封装所有必要的正确性保障手段(比如锁的实现),以确保程序无论在多线程环境下如何调用该方法,将始终保持返回正确的结果。
Java的线程安全
我们在讨论Java的线程安全,实际上讨论的是“相对线程安全”。需要保证的是单独对象操作是线程安全的,调用过程中不需要额外的保障措施,但是涉及到某些业务场景需要特定顺序连续调用,就可能需要调用者考虑使用额外的同步手段保证同步。
例如使用 :
synchronized
Java中与线程同步有关的关键词/类:
1.Synchronized
同步原理:
JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。
任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
Sychronized采取的同步策略是互斥同步
通常情况下,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现形式。在每次获取资源之前,都需要检查是否有线程占用该资源。这里有两个关键点需要注意:
(1)Sychronized是可重入的;
(2)已经进入的线程尚未执行完,将会阻塞后面其他线程;
锁的本质是对象实例
对于非静态方法来说,Synchronized 有两种呈现形式,Synchronized方法体和Synchronized语句块。两种呈现形式本质上的锁都是对象实例。
synchronized (synchronizeDemo)
public synchronized void doSth3() {
我们可以看出,从本质上而非呈现形式上看,synchronized同步也分两种。
(1)锁类的对象实例,针对于某个具体实例普通方法/语句块的互斥;
(2)锁类的Class对象,针对于Class类静态方法/语句块的互斥;
进程切换导致的系统开销:
Java的线程是直接映射到操作系统线程之上的,线程的挂起、阻塞、唤醒等都需要操作系统的参与,因此在线程切换的过程中是有一定的系统开销的。
在多线程环境下调用Synchronized方法,有可能需要多次线程状态切换,因此可以说Synchronized是在Java语言中一个重量级操作。
虽然如此,JDK1.6版本后还是对Synchronized关键字做了相关优化,加入锁自旋特性减少系统线程切换导致的开销,几乎与ReentrantLock的性能不相上下,因此建议在能满足业务需求的前提下,优先使用Sychronized。
2.ReentrantLock (可重入锁)
与Synchronized的实现原理类似,采用的都是互斥同步策略,用法和实现效果上来说也很相似,也具备可重入的特性。
高级特性
(1)公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。Sychronized的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带boolean值的构造函数要求使用公平锁;
(2)锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在Synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要多于一个条件关联的时候,就不得不额外添加一个锁,而ReentrantLock无需这样做,只需要多次调用newCondition()方法即可。
AbstractQueuedSynchronizer(简称AQS)
ReentrantLock实现是基于AQS这个抽象方法的
简单来说,AQS是通过管理状态的方式来实现相对线程安全的。Java中信号量(Semaphore)、读写锁(ReadWriteLock)、计数器(CountDownLatch)以及FutureTask等都是基于AQS实现的,可见这个抽象类的地位多么不一般。
与其说ReentrantLock性能更好不如说Synchronized优化空间更大
上面介绍过,Synchronized在JDK1.6以后性能有所增强,因此在能满足业务复杂度需求的情况下,采用Synchronized也未尝不可。然而互斥同步终究属于悲观的并发策略,在对性能要求极高的业务场景下使用以上互斥同步策略并不合适。
3.volatile 关键字
volatile在多线程环境下保证了共享变量内存可见性。(意思就是线程A修改了volatile修饰的共享变量,线程B能够感知修改。)如果volatile合理使用的话,将会比Synchronized的执行成本更低。
从底层的角度来说,为了提高处理速度,CPU不直接和内存进行通信,而是先将数据读入到CPU缓存后在进行操作,但不知何时将会更新到内存。声明变量加入volatile关键字后,每次修改该变量,JVM就会通知处理器将CPU缓存内的值强制更新到内存中,这就是所谓的“可见性”。
4.Java中的非阻塞同步策略
CAS指令与原子性
原子操作的业务表现形式是“不可被中断或不可被分割操作”。所谓CAS(Compare And Swap)比较并交换就是一种原子操作。简单来说执行CAS需要两个参数,一个新值,一个旧值,当比较内存的值与旧值相符时,则替换为新值,否则不执行替换操作。CPU如何实现,这里不多说,Java若要实现CAS则需要CPU指令集配合。
couter.compareAndSet(0, 1);
System.out.println("结果为" + couter.get());// 结果为1
couter.compareAndSet(0, 3);
System.out.println("结果为" + couter.get());// 结果为1
除了Integer以外,还支持包括CAS更新实例、更新实例的属性等功能。
阅读源码不难发现,Java是通过一个sun.misc.Unsafe的类,完成CAS指令操作的,然而我们从AQS的源码中也发现了sun.misc.Unsafe类的踪影。
其实不难理解,AQS负责管理状态(也可以理解为互斥资源)—— 这里狭义来说可以是锁是否被线程占用的标记,当然,状态的判定规则以及互斥资源数目由AQS的继承者们负责实现,而状态的更新只能是通过CAS指令完成,以确保线程安全。
5.无同步策略
这就比较容易理解了,同步只是线程安全的一个手段,无同步并不意味着线程不安全。大致两种方法的代码可以保证没有使用同步方案的前提下的线程安全。
(1)可重入代码:例如纯计算的函数之类的,方法运行间不需要获取外部资源就可以进行计算 。
(2)线程本地存储资源:线程本地维护自己的资源,根本不存在与其他线程资源冲突的可能。