java多线程进阶同步锁
Posted 烟锁迷城
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程进阶同步锁相关的知识,希望对你有一定的参考价值。
目录
1、原子性问题
一千个线程循环计数,最终结果总是小于等于1000的随机数。.
public class Count
public static int count = 0;
public static void incr()
try
Thread.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
count++;
public static void main(String[] args) throws InterruptedException
for (int i = 0; i < 1000; i++)
new Thread(() -> Count.incr()).start();
Thread.sleep(2000);
System.out.println("结果:" + count);
2、锁
在java中,加锁需要使用synchronized关键字,锁的本质就是对于共享资源访问的一个限制,它让同一时间内只有一个线程能访问这个共享资源,以此确保多线程并发的原子性操作,因此对于synchronized而言,加锁是有作用范围的,范围就是共享资源的使用范围。
2.1、实例锁
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁,只针对于当前对象实例有效。
public class SynchronizedDemo
synchronized void method1()
void method2()
synchronized (this)
2.2、类锁
静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,针对所有对象都互斥,因为静态方法是唯一的,所以在静态方法上加锁也是类锁。
public class SynchronizedDemo
synchronized static void method3()
void method4()
synchronized (SynchronizedDemo.class)
2.3、代码块
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁,如单例模式给HashMap加锁。
public class SynchronizedDemo
Object object = new Object();
void method5()
synchronized (object)
3、锁的存储
synchronize关键字是对某一个对象进行加锁,那么肯定会在某个位置上具有标记,线程可以根据标记判断是否加锁,那么可以确定的就是,标记应该在加锁对象上。
在java中对某一个对象加锁之后,在这个对象的JVM层面的存储结构中,对象头保存有关于锁的信息。
下表是32位存储的内容,64位与32位的存储是几乎没有差别的,可以看到在表中,除了无锁状态之外,还有三种锁,那么一个synchronized具有三种锁类型吗?
4、锁的类型
当两个线程抢占资源的时候,会经历三种类型的锁:偏向锁,轻量级锁,重量级锁,它们是同一把锁的三种状态。
一定要意识到,锁本身就意味着额外的性能消耗,因此为了提升性能,最好的办法是无锁,因此java对synchronized做出了一些优化,三种锁类型就是优化结果。
4.1、乐观锁与悲观锁
4.1.1、乐观锁
乐观锁的预期是乐观的,它默认不会有人修改数据,但是它又无法避免真的可能会有人修改数据,所以它将会比较预期数据和原始数据是否一致,如果一致就修改,不一致就修改失败,下文中CAS就是乐观锁的思想,意为比较并替换。
乐观锁会出现ABA的情况,即数据A被改为B,又被改为A,乐观锁无法发现数据被修改,为解决此问题,可以使用版本迭代的方式来检测是否被篡改过数据,在进行修改时版本上升,在进行比较时同时比较数据和版本
CAS在许多的实现下,其操作必须是原子性的,因为其使用在多线程情况下,进行的本质是两个步骤,比较并替换,这个步骤必须被合成一个原子性操作以保证线程安全。在JAVA实现中,常采用四个参数,object,offset,A,B。offset是偏移量,和Object组合获得内存中的lock_flag,A是预期值,B是要更新的数值,以此做到原子操作。
在CAS上,依旧会有lock,这个lock并非JAVA的锁,而是类似总线锁的操作,以确保多CPU下的安全,此与可见性问题有关。
4.1.2、悲观锁
悲观锁的预期是悲观的,它默认会有人修改数据,所以它会先加锁,然后修改数据。
4.2、偏向锁
偏向锁属于乐观锁,会在加锁但没有产生竞争的情况下使用,进行一次CAS。
偏向锁的存储内容包括线程ID,在单线程的情况下,调用锁资源的线程ID将会被存储在偏向锁中。当一个新的线程开始获取资源时,偏向锁会把新线程ID与保存的线程ID进行对比,如果一致就可以直接获得,不一致将导致该线程的锁膨胀,成为轻量级锁
偏向锁默认关闭,加锁基本就是为了进行多线程竞争处理,所以默认就是会有多个线程,每次比较都会造成性能的消耗,所以默认关闭。
4.3、轻量级锁
轻量级锁属于乐观锁,会在共享资源被抢占的时候使用,进行多次CAS,因为线程调用时间很短,所以相比于阻塞等待,再次进行尝试资源获取的方式会消耗更少的时间。当线程发现锁已经被抢占,就会在间隔一段时间后再次尝试获取,这个过程就叫自旋,自旋的次数是有限制的,通常是10次,以此避免对CPU资源的过多消耗。如果10次之后依然无法获取到资源,将导致该线程的锁膨胀,成为重量级锁。
在1.6之后,优化出自适应自旋,可以自行调整自旋次数
4.4、重量级锁
重量级锁属于悲观锁,非公平锁(允许插队),会在自旋全部失败的时候启用,线程进入阻塞状态。每一个java对象都有一个monitor,每一个线程都有一个监视器Monitor Record,当线程想要获取一个加锁资源时就必须获取到它的monitor,然后将所有权据为己有,直到线程运行完毕才会释放所有权,唤醒被阻塞的线程抢占该资源。准备抢占锁的线程会进入同步队列,没有抢占到资源的线程将进入阻塞状态,随后从同步队列进入到等待队列(wait),等待资源释放后被唤醒(notify),重新进入同步队列,准备进行抢占。
5、synchronized的优化
jdk1.6中对锁的实现引入了大量的优化
- 锁粗化:将紧紧连接在一起的lock指令合成一个
- 锁消除:清除掉没有竞争资源的锁
- 增加轻量级锁和自旋
- 自适应自旋:从原本的10次自旋尝试变成了自适应,即上次等待时间长的将会缩短时间,上次时间短的可以放宽时间。
以上是关于java多线程进阶同步锁的主要内容,如果未能解决你的问题,请参考以下文章