synchronized/volatile/ReentrantLock/ThreadLocal介绍

1、首先先看两篇文章了解一下,篇一篇二

AQS核心思想

2、synchronized介绍

  • synchronized能保证原子性,有序性,可见性。
  • synchronized加锁原理:使用synchronized之后,当执行同步代码块前首先要先执行monitorenter指令,退出的时候monitorexit指令 ,其关键就是必须要对对象的监视器monitor进行获取 ,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor 。
  • 每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一
  • synchronized优化:优化之前的synchronized获取锁(对象的监视器),变现为互斥性,也就是说线程获取锁是一种悲观锁策略 ,而优化之后采用了CAS(compare and swap比较交换) 操作(又称为无锁操作)是一种乐观锁策略
  • CAS没有成功它会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗
  • Jdk1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率 。
  • synchronized可见性:当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见
  • synchronized有序性:synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性
  • synchronized原子性:因为每次只有一个线程在执行,其他线程只能等待 ,所以只能“串行”执行 ,而volatile能保障有序性是通过内存屏障,保障可见性是通过像cpu发送一条硬件指令lock,但是对于一些复杂的业务计算volatile不能保障其原子性

3、volatile介绍

  • volatile能保证有序性,可见性,但不能保证原子性。(后面会讲)
  • volatile可见性:如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令 ,这个指令会个变量所在缓存行的数据写回到系统主内存 ,并且这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效 (是每个处理器通过嗅探在总线观察
  • volatile保障内存有序性:我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。
  • volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

4、为什么volatile不能保障原子性

  • 原子性:原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉**。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
  • 1、 int a = 10; 是原子操作,将10赋值给线程工作内存的变量a
    2、 a++;  读取变量a的值,对a进行加一的操作,将计算后的值再赋值给变量a,这三个操作无法构成原子操作
     
    问题:如何让volatile保证原子性,必须符合以下两条规则:
    1、运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
    2、变量不需要与其他的状态变量共同参与不变约束

5、ReentrantLock介绍

  • ReentrantLock加锁原理:ReentrantLock的实现依赖于Java同步器框架AQS。AQS使用一个整型的volatile变量(命名为state)来维护同步状态。
  • AQS 介绍:全称 AbstractQueuedSynchronizer抽象队列管理器。AQS 中有两个重要的成员,成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。

  • ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞 

  • ReentrantLock还支持公平锁和非公平锁两种方式 ,公平锁满足FIFO
  • 公平锁  VS  非公平锁
    
    1. 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
    
    2. 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,**保证了系统更大的吞吐量**。

6、synchronized与ReentrantLock比较

  • 类别

    synchronized

    ReentrantLock

    存在层次

    Java的关键字,在jvm层面上

    是一个类 java.util.concurrent.locks

    锁的释放jvm自动释放人工手动释放
    锁状态无法判断可以判断,tryLock()方法是有返回值的

    锁类型

    可重入 、不可判断 、非公平锁

    可重入 、可判断、 非公平/公平(默认非公平)

    获取锁底层通过获取对象器,CAS改变计数器通过AQS,AQS实际上是用volatile修饰维护了一个state

 

 

 

 

 7、atmoic原子类介绍

  • atmoic原子类是juc包下的,采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。
  • CAS比较交换的过程可以通俗的理解为CAS(V,O,N),,包含三个值分别为:V 主内存中存放的实际值;O 线程工作内存中的值;N 准备更新的新值。当V和O相同时, 也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过 ,就可以将N值赋值给V值,反之则不作操作。
  • 当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程,CAS的实现需要硬件指令集的支撑

  • 场景举例:假设线程A和线程B同时执行getAndAddInt操作(分别跑在不同CPU上)

  1. AtomicInteger中value原始值为3,即主内存中为3,线程A和线程B各自持有一份value为3的副本分别在各自的工作内存中。

  2. 线程A通过getIntVolatile(var1, var2)拿到value的值3,这时线程A被挂起

  3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,刚好B没有被挂起并执行compareAdeSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B执行完毕。

  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己工作内存中的value3和主内存中的4不一致,说明该值已经被其他线程抢先一步修改过了,线程A本次修改失败,只能重新读取重新来一遍了。

  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

  6. 上述中,比较和修改是容易出错的地方,但是CAS是Unsafe类中的一条CPU系统原语,原语的执行必须是连续的,在执行过程中不允许被中断,即CAS是一条CPU的原子指令,所以比较和交换这一步是不会被其他线程插入的

  • CAS缺点:
  1. CAS的方式相比于锁来说并发性加强了,但如果CAS失败,会一直进行自旋,可能会给CPU带来很大的开销
  2. 只能保证一个共享变量的原子性,对多个共享变量操作可以用锁来保证原子性。

  3. 可能会引发ABA问题,案列场景如下:

  4. 案列:如:t1,t2线程都拷贝到变量atomicInteger=1,如果B线程优先级较高或运气好,
    第一次,t2先将atomicInteger修改为20并成功写入主内存,
    第二次,接着t2又拷贝到atomicInteger=20,将副本又改为1,并成功写回主内存。
    第三次,t1拿到主内存atomicInteger的值。可这个值已经被t2修改过两次
    ABA解决:
    互斥同步锁synchronized
    如果项目只在乎数值是否正确, 那么ABA 问题不会影响程序并发的正确性。
    J.U.C 包提供了一个带有时间戳的原子引用类 AtomicStampedReference 来解决该问题,
    它通过控制变量的版本来保证 CAS 的正确性。
  • atomic包提高原子更新基本类型的工具类,主要有这些
  1. AtomicInteger ,AtomicLong ,AtomicBoolean 这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法
  2. addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;

  3. incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果

  4. getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;

  5. getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;

  • atomic包下提供能原子更新数组中元素的类有:

  1. AtomicIntegerArray ,AtomicLongArray ,AtomicReferenceArray ,这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法
  2. addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
  3. getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
  4. compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新
  • atomic包下提供能引用类型更新的类有:
  1. AtomicReference 原子更新引用类型; ,AtomicReferenceFieldUpdater 原子更新引用类型里的字段; AtomicMarkableReference:原子更新带有标记位的引用类型;
  2.   private static AtomicReference<User> reference = new AtomicReference<>();
    
        public static void main(String[] args) {
            User user1 = new User("a", 1);
            reference.set(user1);
            User user2 = new User("b",2);
            User user = reference.getAndSet(user2);
            System.out.println(user);
            System.out.println(reference.get());
        }
    输出结果:
    User{userName='a', age=1}
    User{userName='b', age=2}
    更改为新对象的值,返回旧对象的值
    
    首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为`User{userName='b', age=2}`,返回的是原来的user对象User`{userName='a', age=1}`。

8、ThreadLocal介绍

  • 简介:在多线程编程中通常解决线程安全的问题我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。事实上,这就是一种“空间换时间”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。
  • ThreadLocal这个类名可以顾名思义的进行理解,表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争

要想学习到ThreadLocal的实现原理,就必须了解它的几个核心方法,包括怎样存怎样取等等,

  1. set方法设置在当前线程中threadLocal变量的值 ,该方法的源码为
  2. ublic void set(T value) {
    	//1. 获取当前线程实例对象
        Thread t = Thread.currentThread();
    	//2. 通过当前线程实例获取到ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
    		//3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
            map.set(this, value);
        else
    		//4.map为null,则新建ThreadLocalMap并存入value
            createMap(t, value);
    }

     

  3. get方法是获取当前线程中threadLocal变量的值,同样的还是来看看源码:

  4. public T get() {
    	//1. 获取当前线程的实例对象
        Thread t = Thread.currentThread();
    	//2. 获取当前线程的threadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
    		//3. 获取map中当前threadLocal实例为key的值的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
    			//4. 当前entitiy不为null的话,就返回相应的值value
                T result = (T)e.value;
                return result;
            }
        }
    	//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
        return setInitialValue();
    }

     

  • 总的来说ThreadLocal是空间换时间的做法,而加锁操作如synchronized和ReentrantLock是时间换空间的做
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值