目录

线程安全的定义:

多个线程访问一个对象 不考虑线程在运行时的调度和交替执行

  • 不要加额外的同步
  • 不要调用方法进行任何其他的协调

调用对象的行为都能得到正确 则是线程安全

Java语言中的线程安全与锁优化

不可变

不可变的对象一定是线程安全的 用final实现的就能保证它是不可变的

绝对线程安全

满足上面的定义 但没有绝对的线程安全

像vector的操作是线程安全的 但在多线程环境中 没有在方法调用(方法的组合)中进行额外的同步措施的话 仍是不安全的

相对线程安全

通常意义的线程安全 对象的单独操作是线程安全的

但特定顺序的连续调用需要使用同步手段保证调用的正确性

线程兼容

对象本身并不是线程安全的 但可以通过在调用段正确使用同步手段实现在并发环境中安全的使用

ArrayList 和HashMap 就是这样的

线程对立

无论调用段是否采取同步措施 都无法在多线程环境中并发使用的代码

线程安全的实现方法

线程安全一方面和代码编写有关 同时也和JVM本身提供的同步和锁机制也有关系 相对来说 后者更加的重要

互斥同步

最常见的一种并发正确性保障手段 > 同步: 多个线程并发访问共享数据 保证共享数据在同一时刻只被一个线程使用

==临界区,互斥量,信号量 是实现互斥的方式==

互斥是因 同步是果 互斥是方法 同步是目的

sync关键字经过编译的字节码会有 两个字节码指令 字节码都要 reference类型 的参数来指明 要锁定和解锁的对象

执行monitorenter指令时 会尝试获得对象的锁 如果对象没被锁定 或当前线程已经有了锁 会将==锁的计数器+1== 执行moitorexit指令时 ==会 -1 计数器为0 进行释放==

注意sync是可重入的 不会锁死自己 Java里的线程是依赖操作系统的原生线程的 阻塞或唤醒 需要操作系统的 处理 包括陷入 状态的转换 但这是非常慢的

还可以使用concurrent 包的 进行实现同步 sync是api层面的 concurent是原生层面 同时还新增了 一些高级功能 ==等待可中断 可实现公平锁 锁可绑定== - 等待可中断: 持有锁的线程长期不释放锁 在等待的线程可以选择放弃等待 改为处理其他事情 - 公平锁: 多个线程在等待同一个锁的时候 必须按照申请锁的时间顺序依次获得锁 - 锁绑定多个条件指 一个ReentrantLock对象可以同时绑定多个Condition对象 sync中

随着Java版本的优化 尽量使用synchonized来进行同步

非阻塞同步

上面的主要影响的是线程阻塞和唤醒带来的性能问题 也是一种悲观的策略 总认为非 同步 即出错

非同步就是基于冲突检测的乐观并发策略 先进行操作 如果没有其他线程争用共享数据 则 操作就成功了 如果没有其他线程抢着用 操作就成功了 如果有争用 就进行补偿措施(不断重试 直到成功为止)

使用乐观并发策略 需要硬件指令集 支持

因为操作和冲突检测这个步骤需要原子性 ==如果使用互斥同步 那就没有必要== 因此就要硬件指令进行支持

硬件保证了 一个语义看起来需要多次操作的行为 只通过一条处理器指令就能够完成

  • 测试并设置
  • 获取并增加
  • 交换

上面3条上世纪已经有了

下面的2条是新增的 - 比较并交换 - 加载链接/条件存储

CAS: 需要3个操作数 1. 内存位置V 变量的内存地址 2. 旧的预期值 A 3. 新值 B

CAS指令执行是 仅仅当V 复合A时 用B更新V 否则不执行更新 无论是否更新 都会返回V的旧值

JDK1.5 之后才能使用 CAS 由 Unsafe进行提供

incrementAndGet()
不断尝试将一个比当前值大1的新值 赋给自己

自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

互斥同步对性能最大的影响就是阻塞的实现 因为 有关线程的挂起与恢复 涉及到了 内核的陷入 于是就给系统的并发性能带来了 一些压力 同时对于某些情况 共享数据的锁定 只会持续很小的一段时间 为了这一小段时间 而去采用挂起和恢复 实在是不划算 这时候就出现了 自旋锁: 让后面的请求的进程进行自旋 不放弃处理器的执行时间 正因为自旋是不会放弃处理器的运行时间

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁的缺点 - 不能代替阻塞,虽然避免了线程切换的开销,但它要占用处理器时间 > 如果被占用的时间很短,自旋等待的效果就会非常好,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源 限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改) 超过次数没有成功得到锁 则就==要挂起线程==

底层实现:

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

自适应锁

自适应说明自旋次数 不是固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定 - 若自旋等待的锁成功获得 并且还在运行 虚拟机会认为很有可能再成功 进而允许自旋等待持续相对更长的时间 - 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

TicketLock、CLHlock和MCSlock

锁消除

对一些代码上要求同步 但是被检测到不可能存在共享数据竞争的锁进行清除

锁清除 主要判定依据来源于逃逸分析的数据支持 若判断在代码中 堆上的所有数据不会逃逸出去从而被别的线程所访问到 则可以把他们当作栈上数据对待 认为是私有的 无需同步加锁

锁粗化

原则上 编写代码 会尽量的将同步块的范围限制的尽量小 只在共享数据 的实际作用域进行同步 这样能够使得需要同步操作的数量尽量的小

轻量级锁

这里的轻量是相对于使用OS的互斥量来实现的传统锁而言

轻量锁的主要目的是为了在==没有多线程竞争==的前提下 减少传统的重量级锁带来的性能消耗

要想了解轻量锁 先来了解下HotSpot虚拟机的对象头

对象头

  1. markWord 非固定的数据结构 用于尽量存储更多的信息

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 2. klass 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 3. 数组长度: 只有数组才有

加锁过程

代码进入同步块的时候 若此同步对象没被锁定 JVM首先将在当前线程的栈帧中 建立一个名为锁(Lock Record)的空间 用以存储锁对象 也是目前的Mark Word 的拷贝

然后JVM使用CAS 尝试将对象的Mark Word更新为指向lock Record 的指针 如果更新成功 则线程拥有该对象的锁 并且标志位变为00 说明是在轻量锁

如果更新操作失败 则会检查对象的Mark Word 是否指向当前线程的栈帧 如果指向 说明之前已经拥有了 直接进入同步块 否则说明这个锁对象已经被其他线程抢占了 膨胀为重量级锁 Mark Word 存储的就是指向重量级锁的指针 后面的进程进入阻塞

解锁

也是利用CAS 若对象的Mark Word仍之喜爱你个线程的锁记录 利用CAS将对象当前的Mark Word 和线程复制的Displaced Mark Word替换回来 替换成功 则完成同步 替换失败 则 释放锁的同时唤醒被挂起的线程

偏向锁

消除无竞争下的同步原语 提高程序的运行性能 偏向锁是在无竞争的情况下把整个同步消除 如果第一个获得这个锁的线程在之后没有 没其他线程获取 则有偏向锁的线程永远不会同步

启用偏向锁 则 锁对象第一次被线程获取会把对象头标志位设为01 同时使用CAS 操作把获得到的这个锁的线程ID 放入对象的Mark Word中 操作成功后 持有偏向锁的线程以后每次进入这个锁相关的同步块 不需要进行同步 当有其他线程争夺的时候 偏向模式宣告结束