互斥同步
互斥同步(Mutual Exclusion & Synchronization)是常见的线程安全的保障手段。
同步定义:保证共享数据在同一个时刻只被一条(或多条,在使用信号量的时候)线程使用。
互斥定义:互斥是实现同步的一种手段,包括:临界区(Citical Section)、互斥量(Mutex)和信号量(Semaphore)。
Synchronized(同步关键字)
Synchronized会在同步块的前后形成MonitorEnter和MonitorExit两个字节码指令,会含有一个Reference类型参数指定一个锁定和解锁的对象,如果没有则会根据修饰的是类方法或者实例方法去取对应的实例或Class对象来作为锁对象。
执行MonitorEnter命令时,会尝试获取对象的锁,如果对象没有被锁定或者已经拥有了对象的锁,则把锁的计数器加1,在执行MonitorExit命令时会将锁的计数器减1。当锁的计数器为0时,锁就被释放了。如果获取失败,那么当前线程会阻塞等待(线程阻塞、挂起是需要调用操作系统级的指令,开销较大),直到对象的锁被其他线程释放。
ReentrantLock(可重入锁)
ReentrantLock与Synchronized都具有线程重入特性。
- 等待可中断,正在等待的线程如果长期获取不到对象的锁,可以放弃等待改作其他事情(lock.lockInterruptibly能够响应中断)。
- 公平锁,必须按照等待时间来一次获得锁;Synchronized是不具备该功能。
- 绑定多条件,可以绑定多个Condition对象来支持多种条件。
ReentrantLock与Synchronized吞吐量性能,在JDK1.6之后就不是那么明显,由于Synchronized的继续优化,优先考虑使用Synchronized关键字。
笔者:总结来讲两者都具有重入性,可以满足大多数场景的使用,除非使用到了可中断等特性,否则推荐使用Sync关键字。
ReentrantLock的父类中会有一个state变量来表示同步的状态, 通过CAS操作来设置state来获取锁,如果设置成了1,则将锁的持有者给当前线程, 如果拿锁不成功,则会做一个申请, 如果还是没有申请到锁,就addWaiter,意思是把自己加到等待队列中去, 其间还会有多次尝试去申请锁,如果还是申请不到,就会被挂起。
Semaphore 对于锁来说,它是互斥的排他的。意思就是,只要我获得了锁,没人能再获得了。可以定义限额。
非阻塞同步
非阻塞同步是基于冲突检测的乐观并发策略,如果产生了冲突,就采用补偿措施(最常见的就是不断重试,直到成功为止),这种并发策略不需要挂起线程。
(非阻塞同步需要硬件指令集的支持,来保证操作和冲突检测是原子性操作)
CAS(乐观锁)
比较并交换操作(Compare and Swap),是通过处理器指令(cmpxchg)来实现的。
算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明经有其他线程做了更新,则当前线程什么都不做。
AtomicInteger:做为线程安全的i++, increaseAndGet();
无同步方案
可以将需要共享的数据可见范围限制在同一个线程之内,最常见的是消费队列(生产者-消费者)模式,将消费过程尽量在一个线程中消费完。
Web交互模型,一个请求对应一个服务器线程的处理。
ThreadLocal线程存储
每一个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以HashCode为键值的K-V值对来找到对应的本地线程变量。
锁的优化
自旋锁,不放弃处理器的执行时间,看看持有锁的线程是否很快会自己释放锁,为了让线程等一会需要让它执行一个自旋(循环)。自旋的时间由虚拟机控制,避免长时间自旋。
锁消除,虚拟机会根据数据逃逸分析来判断是否需要同步加锁,如果不需要则会自动去除掉同步块。
锁粗化,频繁的锁操作会造成过度的资源竞争,建议将多个小锁合并为一个大锁以便减少不必要的损耗。
轻量级锁
轻量级锁会在无竞争的情况下使用CAS操作去消除同步使用的互斥量。
轻量级锁就是为了在无多线程竞争的环境中使用CAS来代替mutex,一旦发生竞争,两条以上线程争用一个锁就会膨胀。设Thread1先拥有锁,Thread2是后来者,这个膨胀可能会发生在Thread1解锁的时候,也可能发生在Thread2尝试加锁的时候。可以看一下Hotspot VM的源码(vm目录下的synchronizer.cpp)。
偏向锁
偏向锁是为了消除数据在无竞争情况下的同步原语,进而提高运行性能。相比轻量级锁,在无竞争状态干脆连CAS都不做了。
该锁偏向当前获得它的线程,如果接下来没有其他线程竞争该资源,偏向锁的线程不再需要同步。
线程
协同式调度,线程的执行时间由线程本身来控制,在把自己的工作执行完了之后,主动通知系统切换到另一个线程。这种方式实现简单,但线程执行时间不可预期,如果线程存在问题会一直阻塞无法切换线程。
抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身决定(Thread.yield方法可以让出执行时间,获得执行时间本身没有办法)。
线程可以指定优先级,优先级越高的越容易被系统选中执行。
线程状态间切换:
原子性:操作指令要么执行成功、要么执行失败,不会出现中间执行一半的状态。
可见性:当一个线程修改了共享变量的值,其他线程能够立即得知修改。
有序性:当前线程观潮所有的操作都是有序的,,但一个线程观察另一个线程所有的操作都是无序的(由于指令重排和工作内存同步延迟的问题)。
Volatile变量
关键字volatile是虚拟机提供的最轻量级的同步机制,其包含两种特性:线程可见性和禁止指令重排。
可见性:当一个线程修改了volatile变量,新值对于其他线程是立即可见的。不会出现幻读、脏读的情况。但类似i++的自增操作由于不是原子性操作,如果并发写也会导致变量的错误,所以volatile关键字标注的变量并不是线程安全的。
volatile关键字读操作,基本没有多余性能消耗;但写操作由于采用了许多内存屏障(Memory Barrier)指令来保证处理器不发生乱序执行,性能稍差。但总体比锁机制要好多。
参考:《深入理解Java虚拟机》
https://my.oschina.net/hosee/blog/614319
https://my.oschina.net/hosee/blog/599884