一,多线程的三大问题和Synchronized的解决
- 可见性
- 原子性
- 有序性
一,三大问题
一,可见性
可见性(Visiblity):指一个线程修改一个共享变量后,其他线程也会得到修改后的值
案例演示:有一个共享变量a,两个线程a2线程修改a为flase,a1则while死循环,假如可见性实现则a2修改值后,a1会立即得到结果退出循环
public class thread_visuality { //共享变量 private static boolean a = true; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while(a) { System.out.println(a); }}).start(); Thread.sleep(2000); //可见性:假如这个线程修改完成后,a1线程退出循环 new Thread(()->{ a=false; System.out.println("已修改!"); }).start(); } }
结果:默认多线程不能保证可见性,a1陷入死循环
二,原子性
原子性:在一次或多次操作中,要么所有的操作都执行并且不会受外界因素干扰。要么所有的操作都不执行
案例演示:一个共享变量a,两个线程A和B分别对a加1000次,默认不加锁多线程是没有原子性的,那么两个线程相加不会一直等于2000
public class thread_atomicity { private static int num; public static void main(String[] args) { Runnable runnable =()->{ for(int a=0;a<1000;a++){ num++; System.out.println(num); } }; for(int i=0;i<2;i++){ new Thread(runnable).start(); } } }
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另一个线程也可能操作共享变量,干扰前一个线程
三,有序性
有序性:指线程中代码的执行顺序,java在编译和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码的顺序
指令重排:jvm的一种优化机制
二,Synchronized解决三大问题
Synchronized可以保证原子性和有序性,但它无法保证可见性,可见性需要依赖Volatile关键字
主要分析保证有序性问题
指令重排的作用:为了提高程序的执行效率,编译器和CPU会对程序中的代码进行重排序
当指令重排遵循as-if-serial语义,并不是所有的指令都进行重排
as-if-serial的含义:不管编译器和CPU如何重排,必须保证在单线程情况下程序的结果是正确的
有数据依赖关系的代码,不能重排序:因为重排序会影响结果
例:以下情况JVM不指令重排,因为有数据依赖关系
① 写后读
int a = 1 int b = a
② 写后写
int a = 1 a = 2
假如重排,结果就为1,但我们想要的结果为2,所有不能重排
③ 读后写
int a = 1 int b = a int a = 2
编译器和处理器不会对存在数据依赖关系的操作进行重排,因为重排会改变结果,但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译和处理器重排序
如:
int a = 1 int b = 2 int c = a + b
c依赖于a和b,所以c不能进行重排序,当a和b没有数据依赖可以进行重排序
synchronized保证有序性的原理:加入synchronized后,虽然进行了重排序,但保证只有一个线程进入了同步代码块,也能保证有序性
二,Synchronized的特性
Synchronized有两大特性:
- 可重入性
- 不可中断性
一,可重入性
一个线程可以多次执行synchronized,重复获取同一把锁
例:演示可重入特性
- 自定义一个线程类
- 在线程类的run方法中使用嵌套的同步代码块
- 使用两个线程来执行
public class synchronizedreentry { public static void main(String[] args) { reentry r = new reentry(); new Thread(r,"wql").start(); new Thread(r,"fq").start(); } } class reentry implements Runnable{ @Override public void run() { synchronized (this){ System.out.println(Thread.currentThread().getName()+"进入了同步代码块一!"); synchronized (this){ System.out.println(Thread.currentThread().getName()+"进入了同步代码块二!"); } } } }
可重入的原理:Synchronized的锁对象中有一个计数器(recursions),它会记录线程获得了几次锁,线程重复获取同一把锁时会被计数,等全部释放后才能让其他线程获取
可重入的好处:
- 避免死锁
- 更好的封装代码
二,不可中断特性
一个线程获取得锁后,其他线程想要获取锁,必须处于堵塞或等待状态,如果第一个线程不释放锁,第二个线程会一直堵塞或者等待,不可被中断
例:演示不可中断特性
public class synchronizednotbreak { private static Object object = new Object(); public static void main(String[] args) throws InterruptedException { //1,定义一个run对象 Runnable run = ()->{ synchronized (object) { System.out.println(Thread.currentThread().getName() + "进入了代码块!"); try { //休眠80000秒 Thread.sleep(80000); } catch (InterruptedException e) { e.printStackTrace(); } } }; //启动线程a Thread a = new Thread(run,"wql"); a.start(); //1000毫秒后在启动b线程 Thread.sleep(1000); Thread b =new Thread(run,"fq"); b.start(); //因为a执行时间很久,把b线程放弃执行 b.interrupt(); //查看b线程的状态 System.out.println(b.getState()); } }
结果:
Synchronized处于堵塞等待的线程不可被中断
三,JVM源码分析synchronized原理
一,反编译Synchronized
在底层synchronized的每一个锁对象都会和一个监视器monitor关联,监视器monitor被占用时会被锁住,其他线程无法来获取该monitor,当JVM执行某个线程的某个方法的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权
Synchronized的底层执行过程:
- 若monitor的进入数为0,线程可以进入monitor,并将monitor的recursions变量设置为加1,owner(所有者)设置为当前拥有线程
- 若线程已拥有monitor的所有权,允许它重入monitor,则recursions变量在进行加1
- 若当前线程已经占有monitor的所有权,那么其他尝试获取monitor的所有权的线程会被堵塞,直到拥有monitor的线程释放,才能重新尝试获取
例:反编译synchronized同步代码块
public class synchronizeddome { private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj){ System.out.println("www"); } } }
编译后:
synchronized底层由两个指令完成:
- monitorenter:尝试获取锁的指令,获取锁的本质就是获取monitor的所有权
- monitorexit:释放锁的指令,本质上也是释放monitor
- JVM保证每个monitorentry必须有对应的monitorexit
monitor的两大部分:
- recursions:当前线程的进入数,当前线程每重入一次就进行加1
- owner:当前的所有者
注:monitorexit指令会被执行两次,一次是方法结束处,一次是异常处
例:反编译synchronized同步方法
public class synchronizeddome { public static void main(String[] args) { } public synchronized void f(){ System.out.println("www"); }}
同步方法反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和monitorexit,在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit
二,JVM源码分析synchronized
一,查看JVM源码的前置步骤
1,JVM源码下载:
- 下载地址:http://openjdk.java.net
- 下载步骤:Mercurial --> jdk8 --> hotspot -->zip
2,JVM是由C编写的,需要下载一个C编辑器(Clion)
3,通过Clion打开下载的JVM源码
二,monitor监视器锁源码
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,其源码是用C++实现的,位于Hotspot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)
ObjectMonitor() { //构造器 _header = NULL; _count = 0; _waiters = 0, _recursions = 0; //线程的重入次数 _object = NULL; //存储该monitor对象 _owner = NULL; //标识拥有该monitor的线程 _WaitSet = NULL; //处于wait状态的线程 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁时的单向列表 FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
三,Monitor的三种行为
一,Monitor竞争
1,执行monitorenter时,会调用InterpreterRuntime.cpp(位于:src/share/vm/interpreter/interpreter.cpp的函数)
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); //判断是不是使用偏向锁,在java这个变量可以设置 if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { //假如没有使用偏向锁,就执行重量级锁这个流程 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); } assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object"); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END
2,对于重量级锁,monitorenter函数中会调用ObjectSynchronized::slow_enter
3,最终调用ObjectMonitor:enter(位于:src/share/vm/runtime/objectMonitor.cpp)
void ATTR ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD ; void * cur ; //通过CAS操作尝试把monitor的_owner变量设置为当前线程 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { // Either ASSERT _recursions == 0 or explicitly set _recursions = 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert OwnerIsThread == 1 return ; } //线程重入,_recursions++ if (cur == Self) { // TODO-FIXME: check for integer overflow! BUGID 6557169. _recursions ++ ; return ; } //如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程 if (Self->is_lock_owned ((address)cur)) { assert (_recursions == 0, "internal state error"); _recursions = 1 ; _owner = Self ; OwnerIsThread = 1 ; return ; assert (Self->_Stalled == 0, "invariant") ; Self->_Stalled = intptr_t(this) ;
步骤:
- 通过CAS尝试把monitor的owner变量设置为当前线程
- 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即为重入锁,执行recursions++记录重入的次数
- 如果当前线程是第一次进入monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回
- 如果获取失败,则等待锁的释放
二,Monitor等待
竞争失败等待调用的是ObjectMonitor对象的EnterI方法(src/share/vm/runtime/objectMonitor.cpp)
源码:
void ATTR ObjectMonitor::EnterI (TRAPS) { ……………… assert (_succ != Self , "invariant") ; assert (_owner != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; //当前线程封装成Objectwaiter对象node,状态设置成ObjectWaitor::TS_CXQ ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; //通过CAS把node节点push到_cxq列表中 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } } TEVENT (Inflated enter - Contention) ; int nWakeups = 0 ; int RecheckInterval = 1 ; for (;;) { if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ; if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; //通过park将当前线程挂起,等待被唤醒 Self->_ParkEvent->park() ; } if (TryLock(Self) > 0) break ; which are exposed to // races during updates for a lower probe effect. TEVENT (Inflated enter - Futile wakeup) ; if (ObjectMonitor::_sync_FutileWakeups != NULL) { ObjectMonitor::_sync_FutileWakeups->inc() ; } ++ nWakeups ;
当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁,TryLock方法实现:
int ObjectMonitor::TryLock (Thread * Self) { for (;;) { void * own = _owner ; if (own != NULL) return 0 ; if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { // Either guarantee _recursions == 0 or set _recursions = 0. assert (_recursions == 0, "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert that OwnerIsThread == 1 return 1 ; } // The lock had been free momentarily, but we lost the race to the lock. // Interference -- the CAS failed. // We can either return -1 or retry. // Retry doesn't make as much sense because the lock was just acquired. if (true) return -1 ; } }
- 当前线程被封装成Objectwaitor对象node,状态设置成ObjectWaiter::s_CXQ
- 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中
- node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还没有获取锁,则通过park将当前线程挂起,等待被唤醒
- 当该线程被唤醒时,会从挂起的点继续执行,通过objectmonitor::trylock尝试获取锁
三,Monitor释放
当某个拥有锁的线程执行完同步代码块时,会进行锁的释放,给其他线程执行同步代码块,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被堵塞的线程,具体实现位于ObjectMonitor的exit方法中(位于:src/share/vm/runtime/objectMonitor.cpp)
源码:
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * Self = THREAD ; if (THREAD != _owner) { if (THREAD->is_lock_owned((address) _owner)) { // Transmute _owner from a BasicLock pointer to a Thread address. // We don't need to hold _mutex for this transition. // Non-null to Non-null is safe as long as all readers can // tolerate either flavor. assert (_recursions == 0, "invariant") ; _owner = THREAD ; _recursions = 0 ; OwnerIsThread = 1 ; } else { // NOTE: we need to handle unbalanced monitor enter/exit // in native code by throwing an exception. // TODO: Throw an IllegalMonitorStateException ? TEVENT (Exit - Throw IMSX) ; assert(false, "Non-balanced monitor enter/exit!"); if (false) { THROW(vmSymbols::java_lang_IllegalMonitorStateException()); } return; } } if (_recursions != 0) { _recursions--; // this is simple recursive enter TEVENT (Inflated exit - recursive) ; return ; } if ((SyncFlags & 4) == 0) { _Responsible = NULL ; } #if INCLUDE_TRACE // get the owner's thread id for the MonitorEnter event // if it is enabled and the thread isn't suspended if (not_suspended && Tracing::is_event_enabled(TraceJavaMonitorEnterEvent)) { _previous_owner_tid = SharedRuntime::get_java_tid(Self); } #endif for (;;) { assert (THREAD == _owner, "invariant") ; if (Knob_ExitPolicy == 0) { OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock OrderAccess::storeload() ; // See if we need to wake a successor if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; } TEVENT (Inflated exit - complex egress) ; if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { return ; } TEVENT (Exit - Reacquired) ; } else { if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock OrderAccess::storeload() ; // Ratify the previously observed values. if (_cxq == NULL || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; }
步骤:
- 退出同步代码块会让_recursions减一,当_recursions的值减为0时,说明线程释放了锁
- 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒
三,Monitor是重量级锁
在看到ObjectMonitor的函数调用中涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒,这个时候就会存在存在系统用户态和内核态的转换,这种切换会消耗大量的系统资源,所以synchronized是java语言中的重量级锁
什么是用户态和内核态,首先需要了解操作系统体系架构:
从上图中可以看到,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核
内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境
用户空间:上层应用程序活动的空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源,存储资源,I/O资源等
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口,即:系统调用
所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态),但是当它调用系统执行某些操作时,例如:I/O调用,此时需要陷入内核中运行,这个时候处于内核运行状态(简称:内核态)
简单过程:
- 用户态程序将一些数据放在寄存器,或者使用参数创建一个堆栈,表明需要系统提供资源和服务
- 用户态程序执行系统调用
- CPU切换内核态,并跳到位于指定位置的指令
- 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务
- 系统调用完成后,操作系统会重新切换CPU为用户态并返回系统调用结果
用户态切换到内核态需要传递许多变量,同时内核还需要保护用户态在切换时的一些寄存器值,变量等,以备内核切换回用户态,这种切换就带来了大量的系统资源消耗,所以在Synchronized未优化之前,效率低的原因
三,Synchronized锁升级
高效并发是JDK1.5到JDK1.6的一个重要改进,Hotspot虚拟机实现了锁优化,包括偏向锁(Biased Locking),轻量级锁(Lightweight Locking),自适应自旋(Adaptive Spinning),锁消除(Lock Elimination),锁粗化(Lock Coarsening)等,这些技术都是为了线程之间更高效的共享数据,以及解决竞争问题,提供程序的执行效率
锁升级的过程:偏向锁 --> 轻量级锁 --> 适应性自旋 --> 重量级锁
一,偏向锁
偏向锁是JDK1.6的重要引进
偏向锁实行的原因:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低,所以引入了偏向锁
偏向锁的含义:偏向锁就是偏向于第一个获取它的线程,会在对象头存储锁偏向的线程ID,以后线程进去和退出同步代码块只需要检查是否位偏向锁
偏向锁在Java 1.6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 XX:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过xX:-UseßiasedLocking=false参数关闭偏向锁
例:演示激活偏向锁之后的对象布局
① 实例
public class lockobj extends Thread { private static Object lock = new Object(); @Override public void run() { for (int a=1;a<10;a++){ synchronized (lock){ //打印这个锁对象的布局 System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } } } }
②测试
public class dome1 { public static void main(String[] args) { lockobj lockobj = new lockobj(); lockobj.start(); } }
③结果
偏向锁的原理:
- 虚拟机将对象头中的标志位设为"01",即为偏向模式
- 同时使用CAS操作把获取这个锁的线程ID记录到对象头的Mark Word的偏向锁中,如果CAS操作成功持有偏向锁的线程以后每一次进入锁同步块时,虚拟机都可以不再进行任何同步操作
CAS(比较并交换):比较对象头中偏向锁中的线程ID和当前获取的线程是否一致,一致的话就为偏向锁操作,不一致说明有多个线程,就对锁进行撤销
偏向锁一旦出现多个线程竞争时就会被撤销,转化为轻量级锁
偏向锁的撤销:
- 偏向锁的撤销必须等待一个全局安全点
- 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 撤销偏向锁,恢复到无锁(标志位位01)或者轻量级锁(标注为00)的状态
偏向锁的优点:偏向锁在只要一个线程执行同步代码块时进一步提高性能,适应于一个线程反复获取同一把锁的情况
二,轻量级锁
轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统,的锁机制就称为“重量级“锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能满耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁
轻重级现原理当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
获取锁步骤如下:
- 判断当前对象是否处于无锁状态(hashcode, 0, 01),如果是,则JVM首先将在当前线程的栈顿中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前级,即Displaced Mark Word) ,将对象的Mark Word复制到栈赖中的Lock Record中,将LockReocrd中的owner指向当前对象
- JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作
- 如果失败则判断当前对象的Mark Word是否指向当前线程的栈赖,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态
锁的释放:
- 取出在获取轻量级锁保存在Displaced Mark Word中的数据
- 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功
- 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁
对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。轻量级锁好处在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
三,适应性自旋
在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚
成力获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理播资源,有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越聪明"了
四,锁的消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做堆上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步
public class dome1 { public static void main(String[] args) { contactString("a","b","c"); } public static String contactString(String s1, String s2, String s3){ return new StringBuffer().append (s1) . append (s2). append(s3).toString(); } }
StringBuff的append方法源码:进行了synchronized同步操作
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
StringBufferBappend ()是一个同步方法,锁就是this也就是(new StringBuilder()),虚拟机发现它的动态作用域被限制在concatString()方法内部,也就是说.new StringBuilder()对象的引用永远不会"逃逸"到concatString()方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了
五,锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗
锁粗化演示:
public class dome1 { public static void main(String[] args) { StringBuffer stringBuffer = new StringBuffer(); //append方法是同步方法,循环多次对同步方法进行获取和释放锁操作,这个时候可以锁粗化,对循环加锁即可 for(int i=1;i<=10;i++){ stringBuffer.append(i); } System.out.println(stringBuffer); } }
锁粗化,JVM会探测到一连串细小的操作都使用同一个对象加领,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可
四,Synchronized锁优化
Synchronized锁优化有两个出发点:
- 本质上就是避免锁升级到重量级锁,因为重量级锁相比较而言极其耗资源
- 对具体的操作能拆分锁就拆分锁,能不用锁就不用锁
一,优化一:减少锁的范围
同步代码块的操作尽量少,代码尽量短,减少锁的竞争,让它在轻量级锁和适应性自旋中就完成操作
synchronized (lock){ System.out.println("");//代码尽量短 }
二,优化二:减低锁的粒度
将一个锁拆分成多个锁,因为默认多个锁除了this为本class,锁对象是不一样的,可以提供并发量
以HashTable和ConcurrentHashMap来论证:
HashTable源码:
public synchronized V put(K key, V value) { if (value == null) { throw new NullPointerException(); } …………………… addEntry(hash, key, value, index); return null; } public synchronized V get(Object key) { …………………… } return null; } public synchronized V remove(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; …………………… } } return null; } 省略………………
HashTable它的put和get等都就行了加锁操作,同步方法锁为this表示锁住当前对象,那么一个线程put另一个线程即不能put也不能get,只能等待锁被释放,严重影响效率
ConcurrentHashMap源码:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); ……………… else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { …………… } } } addCount(1L, binCount); return null; } public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
ConcurrentHashMap对于get读操作并没有加锁,因为读取共享变量其实并不会引发线程安全问题,put方法使用的是CAS比较和Synchronized同步代码块,一个线程写时,不影响另一个线程读,而且ConcurrentHashMap锁对象是map中node表示加入多个线程不是操作map中的同一个节点的数据也不会堵塞,大大提供性能
三,优化三:读写分离
线程安全问题的根本原因:
- 多个线程操作共享的数据
- 操作共享数据的线程代码有多条
- 多个线程对共享的数据都具有写操作
读操作虽然操作了共享数据,但并没有影响数据,本质上没有线程安全问题,读写分离在其他框架和数据库经常被用到
Comments | NOTHING
Warning: Undefined variable $return_smiles in /www/wwwroot/wql_luoqin_ltd/wp-content/themes/Sakura/functions.php on line 1109