synchronized原理分析
synchronized锁升级的过程
- synchronized做了大量的改进,会完成一个锁升级,先是由无锁状态升级为偏向锁,然后再是轻量级锁,最后是重量级锁。
- 如果当前线程进来之后,会通过一个CAS,替换成自己的线程id,如果每次进来都是这个线程,以后就只要做一个简单的比较就可以了,不需要等待和唤醒,也不需要挂起,效率非常高,和无锁状态之间调度差不多。
- 如果再来一个线程,两个线程一竞争,就会发生多次CAS,这个时候就会升级为一个轻量级锁。因为这两个线程交替执行,执行时间比较短,因此也没必要让另一个没拿到锁的线程等待和唤醒,会让另一个线程进行自旋,不会去释放锁,等拿到锁的线程执行完了,再通过CAS获取锁,效率就提高了,只需要几次CAS就可以完成加锁和解锁的操作。
- 如果线程多变了,几百个几千个。而当前只有一个线程在运行,如果你不进行升级的话,其他线程就都需要进行自旋,而自旋是需要占用CPU资源的,线程量一大,就非常耗费性能,所以这个时候就会升级为重量锁,让拿到锁的线程执行,其他所有线程阻塞,等待唤醒。
synchronized底层
synchronized底层是由一个monitor对象来控制的,而monitor完整来讲是叫ObjectMonitor对象,是由C++实现的,里面有主要几个变量,有一个 ,当线程生成的时候,会进入 ,此时线程状态全是blocking,然后线程拿到锁之后,会获取monitor对象的监视器,此时线程就是running状态,owner就会执行当前运行的线程,count就会+1。如果调用了wait()方法,就会释放锁,owner就会变成null,count就会-1变成0,同时线程会进入WaitSet中,等待被唤醒,这就是整个实现流程。
synchronized一般用来修饰代码块,同步方法。
修饰代码块:
修饰代码块的时候,实际上是由两个指针完成的,monitorenter代表开始的位置,monitorexit代表结束的位置。通过这两个指令,来获取monitor锁的持有,如果持有了,count就+1,会进入到运行队列,owner会执行当前运行线程,退出之后,会让其他的线程进行持有锁。
修饰同步方法:
如果是修饰同步方法,它就会变成一个标识符,然后就是判断这个标识符,如果有这个标识符,也是会去尝试获取monitor的持有权,如果能够获取到monitor对象,它就拿到锁了,原理也是一样的,count就+1,会进入到运行队列,owner会执行当前运行线程。
这两个虽然表现形式不一样,但最终本质都是来获取monitor对象,锁的持有权。这些最终都能够体现在java对象,因为java对象的本身结构就是对象头、实例变量和填充字节,对象头里面又包含两个部分,MarkWord 和 KlassPoint(类型指针) ,MarkWord里面记录了一些数据结构,锁的标志位。无锁状态下,记录的是hashCode,,主要是看是否是偏向锁以及标志位。当升级为偏向锁之后,是否是偏向锁的值就是1,标志位是01,通过这个判断,就知道当前是偏向锁,再判断threadId,如果是轻量级锁,它记录的是栈帧中锁记录的指针,所以两个线程会通过CAS来替换这个锁记录的指针,通过判断这个指针是否替换成功,进而来判断是否是拿到锁。如果是升级为重量级锁,在MarkWord中存储着的是重量级的指针,都是栈帧中的指针,这个就是他的对象头结构。
CAS
cas是一种能让线程不需要通过阻塞就能够避免多线程安全问题的一种算法,也是乐观锁的技术。它可以不使用锁而保证多线程安全。所以cas也是一种无锁算法。
CAS实现:
当且仅当内存值==预估值,则更新更新值
就是在修改的时候,判断一下内存值是否等于预估值,如果相等,就修改更新值
CAS存在一个ABA问题
1.第一个线程的时候,内存值和预估值都是5,然后就会执行修改操作,但是就修改操作的执行时间会过长
2.第二个线程开始执行,内存值和预估值是5,执行修改操作,将更新值改成了6
3.第三个线程开始执行,内存值和预估值是6,执行修改操作,将更新值改成了5
4.此时第一个线程执行修改操作,修改更新值
解决方案:加个版本号
偏向锁、轻量级锁、重量级锁
偏向锁
当只有一个线程访问同步代码块时,第一次获得锁之后,之后就不需要获得锁了,可以减少不必要的CAS操作
轻量级锁
当出现多个线程竞争锁时,不会让线程在阻塞与唤醒状态间切换,而是让线程自旋
自旋会占用CPU资源,太多的线程自旋会损害CPU,所有自旋超过一定次数,就会变成重量级锁
重量级锁
线程不会自旋,线程竞争锁失败后,会让线程等待,造成阻塞