java多线程学习笔记 --二.线程安全性

线程安全性

线程安全性概念

在多线程访问的情况下,程序还能够按照预期的行为去执行,(跟串行执行结果一样)

原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作

可见性:一个线程对主内存的修改可以及时的被其他线程观察到

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

原子性

原子性–atomic包

代码演示 count包

AtomicXXX:CAS 原子增加的核心代码

这个方法 compareAndSwap()是CAS 这个方法的原理是放到一个死循环里一直尝试修改目标值直到修改成功

//incrementAndGet()->unsafe.getAndAddInt(this, valueOffset, 1) + 1;
public final int getAndAddInt(Object var1, long var2, int var4) {//var1:countDownLatch
    int var5;//在这里假设var2=2 var5就是底层中var2中的值是2
    do {
        var5 = this.getIntVolatile(var1, var2);//底层传过来的值 
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//如果底层和目前的值相等(var2 = var5)就用var5 + var4替换var2也就是说var2或者是var5加了一个1 然后用这个新值把底层覆盖掉,如果var2!=var5,则把目前var1对象中最新的值赋值给var2,继续判断(注释:count对象的值是工作内存中的值,底层的值是主内存中的值)
    return var5;
}
LongAdder与AtomicLong

这个类相比于普通的Atomic类

当竞争不激烈时Atomic类修改目标值 成功多,竞争激烈时 失败多,一直循环影响性能

LongAdder原理 普通类型的double和long jvm将64位的读写操作拆成2个32位操作,将热点数据分离,分成好多cell,每个cell维护自己的值,每个cell+1,最终的结果将所有的cell累加合成

优点:在高并发将单点的更新压力分到各个节点上,在低并发时,对base直接修改和Atomic类差不多

缺点:在统计时有并发更新,会导致误差

综合:在线程竞争不激烈且低并发时使用Atomic ,竞争激烈时高并发时采用LongAdder

AtomicReference与AtomicReferenceFieldUpdater

代码中展示了具体用法package com.mmall.concurrent.example.atomic;

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性

AtomicReferenceFieldUpdater是一个基于反射的类,它允许原子更新指定类的指定voltile字段

tip:虽然说jvm规范说64位原子操作会拆分为2个32位的原子操作,但现在的虚拟机都支持64位的原子操作

AtomicStampReference:CAS的ABA问题

当进行CAS时 本来内存的值是A 后来又被其他线程改成了B,然后又有一个线程将其改成A,所以CAS时看到了取得的值和本身的值一样,成功CAS。但这与我们的初衷不一样。所以我们增加一个stamp,这个变量相当于要操作资源的版本号,这样能避免ABA问题。

演示AtomicBoolen的用法

主要用于某一段代码只执行一次的情况

具体请看代码演示

原子性–锁

锁的类型
悲观锁和乐观锁

这两种描述的是锁的用法

悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。实现方法:一些独占锁,Synchronized Reentrantlock

乐观锁:次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。实现方法:CAS,版本号机制

在这里插入图片描述

自旋锁

定义:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,不断判断锁是否能够成功获取,直到获取锁才会退出循环。

优点和缺点

优点:线程等待时不会sleep,减少了不必要的上下文的切换,执行速度快,非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换

缺点:

1)如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高

2)上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题

互斥锁

和自旋锁差不多,差异在于当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将进入睡眠状态

主要提供了互斥访问

可重入锁

比个例子:一个类的A,B两个方法都被sychronized修饰,A方法中又调用了B方法,当调用A方法时,如果是不可重入锁,则B方法不会被调用,那么就会产生死锁。如果是重入锁那么调用A方法时,B方法的锁也会被拿到,会被如期调用

指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以获取本对象上的锁,而其他线程不可以。

实现原理是jvm底层实现一个计数器,当有锁获得便+1,当线程释放锁便-1,当计数器为0时,锁被释放。

Synchronized和ReentrantLock都是可重入锁

synchronized和ReentrantLock 都是可重入锁。ReentrantLock与synchronized比较:

1.前者使用灵活,但是必须手动开启和释放锁

2.前者扩展性好,有时间锁等候(tryLock( )),可中断锁等候(lockInterruptibly( )),锁投票等,适合用于高度竞争锁和多个条件变量的地方

3.前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。

偏向锁

顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁的恢复成轻量级锁。 适用于无竞争

它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。轻量级锁包括自旋锁。 适用于竞争不激烈

重量级锁

主要为互斥锁和独占锁 适用于竞争激烈


锁有synchronized 和 lock

sychronized
  • 修饰代码块:大括号括起来的代码,作用于调用对象
  • 修饰方法:整个方法,作用于调用对象
  • 修饰静态方法:整个静态方法,作用于所有对象
  • 修饰类:括号括起来的部分,作用于所有对象
sychronized 与 Lock对比
  • sychronized:不可中断锁,适合竞争不激烈,可读性好
  • lock:可中断锁,多样化同步,竞争激烈时能维持常态
  • atomic:竞争激烈时能维持常态,比lock性能好;只能同步一个值

可见性

导致不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

可见性 – synchronized

JMM关于synchronized的两条规定
  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)

可见性 – volatile

通过加入内存屏障禁止重排序 优化来实现

  • 对volatile变量写操作时,会在写操作后加入一条store屏障命令,将本地内存中的共享变量值刷新到主内存。
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

可见性 – volatile写

在这里插入图片描述

可见性 – volatile读

在这里插入图片描述

可见性 – volatile使用

适用于标识量

volatile boolean inited = false;
//线程1:
context = loadContext();
inited= true;
// 线程2:
while( !inited ){
 sleep();
}
doSomethingWithConfig(context)

用于构建单例模式 双重检测机制的时候使用

有序性

概念

java内存模型中,允许编译器和处理器对指令进行重排序*** ,但是重排序的过程不会影响到单线程*程序的执行,却会影响多线程的并发执行的正确性

可以用volatile,synchronized,lock保证有序性

有序性 – happens-before原则

  • 程序次序规则:哪个先写哪个先执行

注意:指令的重排序是在不影响依赖时进行的

  • 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作

  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  • 传递规则:如果操作a先行发生于操作b,而操作b又先行发生于操作c,则可以得出操作a先行发生于操作c

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值