盒子
盒子
文章目录
  1. Java 中的锁
  2. Java 线程通信
  3. Java 内存模型 (JMM)
  4. Java 中的类与关键字
    1. 简单区分一下
    2. synchronized volatile
      1. 简单对比
      2. synchronized
        1. 实现原理
          1. Java 对象头
          2. Monitor
        2. 锁的升级
        3. synchronized 的四种状态
      3. volatile
        1. 介绍
        2. 实现
        3. 原子操作
    3. Synchronized 与 ReenTrantLock
      1. synchronized
        1. 介绍
        2. 使用
        3. 实现
        4. 锁的状态
        5. 特点
      2. Lock
        1. 介绍 A
        2. 使用
        3. 介绍 B
          1. 比较
          2. 接口 Lock
            1. 实现 ReentrantLock
            2. 接口 ReadWriteLock
            3. 实现 ReentrantReadWriteLock
        4. ReentrantLock
        5. ReentrantReadWriteLock
    4. 锁的类型
      1. Synchronized 是啥子锁
      2. ReentrantLock
      3. ReadWriteLock
      4. java.util.concurrent.atomic 包下的原子类
  5. 锁的相关概念
    1. 乐观锁/悲观锁
      1. 悲观锁
        1. 用于
      2. 乐观锁
        1. 用于
        2. 实现方式
          1. 版本号机制
          2. CAS 算法
        3. 缺点
    2. 自旋锁
    3. 独享锁/共享锁
    4. 互斥锁/读写锁
    5. 公平锁/非公平锁
      1. 公平锁
      2. 区分
    6. 可重入锁
    7. 读写锁
    8. 可中断锁
    9. 偏向锁/轻量级锁/重量级锁
    10. 分段锁
    11. synchronized 升级后的三种锁
      1. 锁的优缺点对比
      2. 偏向锁
      3. 轻量级锁
      4. 重量级锁
  6. 并发编程的三个概念
    1. 原子性
    2. 可见性
    3. 有序性
    4. 指令重排序
  7. Java 与 CAS
  8. JVM 对锁的优化
    1. 锁消除
    2. 锁粗化
  9. 其他

Java 并发 锁

Java 中的锁

讨论锁,首先要知道 是为了解决啥问题产生的.

锁,是为了避免线程间共享资源出错而产生的.

在 Java 中,从 jdk1.0 开始,每个 Java 对象都有一个内部锁.

比如如果一个线程访问了某个 Java 对象中的数据,此时该线程获得该对象的锁,其余线程将无法访问该对象的数据.

这样说不严谨,实际上比如,一个线程访问了一个对象由 synchronized 修饰的方法,线程就获得该对象的锁,那么其它的线程想要访问由 synchronized 修饰的方法就因为获取不到锁而阻塞.但是,其他线程此时是可以访问非同步方法的.因为无需获取锁.

锁分为很多种.有的锁觉得资源不会发生变化,所以允许多个线程获取该资源.但是一个资源可能发生变化,那么就不能让多个线程获取某个资源了,因为这样可能产生一个线程修改了某资源的值,而另一个线程仍使用着某资源之前的值,从而产生问题.(或者另一个线程使用的值也被修改了,同样会出错.)

Java 的锁都是是对象锁.有的地方说当通过锁机制访问静态方法或数据时(方法声明为 synchronized),此时获取的是类锁.通常我们认为静态方法或属性是类的属性,而普通方法与属性,是对象的属性.参考 Java 类加载 ,我们知道,类在 JVM 中也是一个对象的形式存在的.所以所谓的”类锁”也是对象锁,只不过是类加载进 JVM 后的类对象的锁.

若一个方法用 synchronized 声明,则对象的锁保护整个方法.

对象的锁用来管理试图调用方法的线程.

Java 线程通信

上面提到 锁,是为了避免线程间共享资源出错而产生的.

线程共享资源的一种应用,就是 Java 的线程通信.即 共享内存模型 .

这里联系操作系统的 线程通信

而 Java 线程通信则是由 JMM(Java 内存模型) 类控制的.

Java 内存模型 (JMM)

JVM 尝试定义 JMM 来屏蔽各个硬件平台和操作系统的内存访问差异,以实现 Java 程序在各个平台下都能达到一致的内存访问效果.

JMM 规定所有的变量都存放在主存(物理内存),每个线程都有自己的工作内存(高速缓存).

线程对变量的操作必须在工作内存,不能直接对主存进行操作. –> 所以需要把工作内存的变量值同步至主存.

每个线程不能访问其他线程的工作内存.

上面在 Java 中的锁 中提到一个线程共享资源可能出错的场景,联系这里,可以知道,如果两个线程共享一个资源,那么一个线程修改了自己工作内存的资源,然后把该资源同步到主存.此时另一个线程使用的自己工作内存的资源还是之前的值,这里就可能出现问题.

实际上,会有一个不断触碰,判断工作内存的资源与主存是否一致的过程,当发现不一致,会把主存的值更新到工作内存.这样,也会出错.

Java 中的类与关键字

下面的内容有重复,但是为了从不同的方面对比,是必要的…

简单区分一下

synchronized,volatile –> 是线程同步的关键字

Lock –> 一般来说,是 Java 的锁实现

线程同步可能会使用到锁

而 java.lang.concurrent.atomic 包下的原子操作类 没有使用锁 而是使用 CAS 的方式来保证原子性.

synchronized volatile

简单对比

Java 使用的并发机制 依赖于 JVM 的实现和 CPU 的指令.

volatile 的实现是在 cpu 层面,使用了 lock 前缀的指令…

synchronized 的实现是在 JVM 层面,使用了 monitorenter,monitorexit…

具体怎么回事,看下面…

PS : 而 ReenTrantLock 是 JDK 实现的

synchronized

实现原理

在 JVM 规范中可以看到 synchronized 在 JVM 里的实现原理.

JVM 基于进入和退出 Monitor 对象来实现 方法同步代码块同步.

但两者的实现细节不一样…

代码块同步 使用 monitorenter 和 monitorexit 指令实现…

方法同步 使用另一种方式,细节在 JVM 规范未说明.但是方法的同步也可以用上述俩指令实现.

synchronized 使用的锁存在 Java 对象头的 Mark Word 里…

早期版本中,synchronized 属于重量级锁,在操作系统层面的实现用到了 Mutex Lock,线程之间的切换需要从用户态转换到核心态,开销较大.

jdk6 做了优化.

Java 对象头和 Monitor 是实现 synchronized 的基础.

Java 对象头

对象在内存中的布局

  • 对象头
  • 实例数据
  • 对齐填充

具体 看 JVM 类文件结构

简单讲,锁的各种状态存放在 Java 对象头 的 Mark Word 块里.

Monitor

一种同步机制?

JVM 中用 C++ 实现

jdk1.6 为 synchronized 做了优化,引入了 偏向锁,轻量级锁…

什么是 偏向锁,轻量级锁?

锁的升级

  1. 偏向锁
  2. 轻量级锁
  3. 重量级锁

[看下面锁的相关概念]

synchronized 的四种状态

  • 无锁,偏向锁,轻量级锁,重量级锁

锁膨胀的方向 : 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

volatile

介绍

使用 volatile 在某些情况下比使用锁更方便.

与 JMM(Java 内存模型) 对比

与 Java 的锁机制联系

如果一个字段被声明为 volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的.

一个域(类的成员变量,类的静态成员变量)声明为 volatile :

  • 保证了不同线程对这个变量进行操作的可见性.

    若一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的.

  • 禁止进行 指令重排序 .

  1. 能保证可见性 –> 读取的都是最新的值
  2. 不能保证原子性
  3. 一定程度保证有序性

原子性使用 java.lang.concurrent.atomic 包下的原子操作类.该包下的类没有使用锁,而是使用 CAS 的方式来保证原子性.

注意 : 原子操作类, CAS

不过啥是原子性,有序性…呢?

这是并发编程的三个概念 见下文 三个概念

实现

Java 代码编译为 Java 字节码,字节码被类加载器加载到 JVM,JVM 执行字节码,最终需要转化为汇编指令在 CPU 上执行.

volatile 修饰的变量进行 写操作 时,是通过 cpu 级别实现的.

查看汇编指令会发现多出 lock 前缀的指令…

实现了

  • 把当前处理器缓存行的数据写回系统内存.
  • 这个写回内存的操作会使其他 cpu 里的缓存无效.

lock 前缀指令实际上相当于一个内存屏障(也称 内存栅栏).提供三个功能

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

与操作系统联系

每个处理器通过 嗅探 在总线上传播的数据来检查自己缓存的值是否过期,当处理器发现自己缓存行对应的内存地址被修改,将会将当前处理器的缓存行设置成无效,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里.

原子操作

jdk1.5 开始 Java 增加了 atomic 包下的原子类

这些类使用 CAS 保证原子性

CAS 是什么?会出现什么问题?

处理器如何实现原子操作…??

  • 总线锁定
  • 缓存锁定

[回头再看看…]

jdk1.5 后, Java 可以在程序中使用 CAS,该操作由 sun.misc.Unsafe 类里的 compareAndSwapInt() 等方法包装提供.虚拟机在内部做了特殊处理,即时编译出来的结果就是一条平台相关的处理器 CAS 指令.

由于 Unsafe 类不是面向用户调用的类(而是面向 jdk 开发者),故只能通过反射或者其它的 Java api 来间接使用.

如 java.util.concurrent.atomic 下的原子类,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// java.util.concurrent.atomic.AtomicInteger

/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

unsafe.compareAndSwapInt() 是个 本地方法.

Synchronized 与 ReenTrantLock

Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

synchronized

介绍

因为 jdk6 对 synchronized 有更新,特性应当与 jdk5 之前对比来看…

[待续…]

使用

  1. 修饰普通方法 –> 获取对象内置锁
  2. 修饰代码块 –> 获取对象内置锁
  3. 修饰静态方法 –> 获取类(类的字节码文件对象)锁

说明一下 :

static 修饰的成员,也就是静态成员,是类的属性.

普通的方法,成员变量,是类的具体实例,也就是对象的属性.

但是在 JVM 中,类和普通的实例,都是对象.

比如反射

1
2
Class cls = Class.forName("Sutudent");// 获取一个类对象
Student stu = (Student)cls.newInstance();// 使用类对象的方法获取一个类的实例

类对象,即类的字节码文件对象.

而每个对象,都有一个内置锁.

事实上,获取的都是对象锁.但是为了区分,把实例的对象锁称为对象锁,把类对象的锁称为类锁.

实现

上面有提到过

在 JVM 规范中可以看到 synchronized 在 JVM 的实现原理.

JVM 基于 进入 和 退出 Monitor 对象来实现 方法 和 代码块 的同步,但两者实现细节不一样.

代码块同步使用 monitorenter 和 monitorexit 指令实现…

方法同步使用另一种方式,JVM 规范未说明.但是方法的同步也可以用上述俩指令实现.

….

monitor

对象头保存信息…

类的字节码信息,格式…

在操作系统使用啥来实现的??

Mutex Lock…

….

锁的状态

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

特点

synchronized 基于 隐式获取锁,这是优势,也是面对其它方式的缺点.

隐式获取锁 的意思 : 无需显式获取锁,释放锁

PS : 这特么不是废话吗

也就是不需要的手动写代码获取,释放…

优点 : 便捷

缺点 : 扩展性低 –> 与 Lock 比较

  1. 方法,代码块执行完毕自动释放锁.
  2. 遇到异常,其持有的锁自动释放 –> JVM 操作.
  • 内置锁的可重入性

    与 Lock 的可重入锁 ReentrantLock 对比

    支持一个线程对资源的重复加锁.

    因为锁的持有者是线程,而非调用.

    所以 : 一个线程对一个资源加锁了,下次还能继续进入这个锁.

    相当于它能开锁,进入,再加锁. –> 这就是重复加锁(可重入)的意思.

Lock

介绍 A

Lock 是一个 接口,jdk1.5 之后引入.

与 Lock 相关的接口,类 都放在 java.util.locks 包下.

但是值得一提的是,jdk1.5 之后还与 Lock 一起引入了 ReadWriteLock 接口.

下面谈到的 ReetrantLock 是实现了 Lock 的类.

而 ReentrantReadWriteLock 是实现了 ReadWriteLock 接口的类.

很多资料把 Lock,ReetrantLock,ReentrantReadWriteLock 一起谈,搞得像后两者都是 Lock 的实现一样.虽然从概念上讲差不多,但是实际的实现并不是那么想当然.

使用

使用,肯定是使用接口的具体实现类.

  1. 可重入锁 ReetrantLock
  2. 读写锁 ReentrantReadWriteLock

可重入锁,读写锁…都是锁机制的一些概念.而 ReetrantLock 是通过 Lock 对可重入锁的 Java 实现,其他同理…

那锁有哪些概念呢??看下面 锁的相关概念

使用方式,比如

1
2
3
4
5
6
7
Lock lock = new ReetrantLock();
lock.lock();
try{

}finally{
lock.unlock();
}

介绍 B

我们回头来看 java.util.locks 包.

比较

jdk1.5 添加了 Lock 接口,提供了 Synchronized 相似的同步功能,只是使用需要显式获取,释放锁.

所以可以简单理解为 Lock 是为了解决 Synchronized 的效率问题引入的.

而 ReadWriteLock 是读写分离锁,啥是读写分离锁呢?(也称 读写锁)

读写分离锁 指 同一时刻允许多个线程进行读访问,但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.

既然单独说 ReadWriteLock 是读写锁,那么 ReentrantLock 就不是读写锁啦.ReentrantLock 是一种 排他锁 .顾名思义,无论是读还是写,都只允许一个线程访问.

读写锁 可以有效的帮助减少锁的竞争,提升系统性能。

接口 Lock
1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
实现 ReentrantLock
1
2
3
public class ReentrantLock implements Lock, java.io.Serializable {
//...
}
接口 ReadWriteLock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();

/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
实现 ReentrantReadWriteLock
1
2
3
4
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
//...
}

ReentrantLock

//具体谈谈…

看源码,实现提到了 AbstractQueuedSynchronizer,即 AQS…

单独了解一下吧.

ReentrantReadWriteLock

具体谈谈…

锁的类型

参考下面锁的相关概念.

Synchronized 是啥子锁

  1. 悲观锁
  2. 非公平锁
  3. 不可中断锁.
  4. 可重入锁 —. 是隐式的,获取了锁,直接再用就是了.

阻塞算法

ReentrantLock

  1. 独享锁
  2. 互斥锁
  3. 默认情况下是非公平锁,但是可以设置为公平锁.
  4. 可重入锁 –> 调用了 lock() 方法获取到锁的线程,可以再次调用 lock() 获取锁而不被阻塞.
  5. 不可中断锁.
  6. Lock 是可中断锁.使用 ockInterruptibly().

ReadWriteLock

  1. 读锁是共享锁,其写锁是独享锁。
  2. 读写锁
  3. 默认情况下是非公平锁,但是可以设置为公平锁.

java.util.concurrent.atomic 包下的原子类

  1. 乐观锁
  2. 建立在CAS之上的
  3. 非阻塞算法

锁的相关概念

参考这里

从 锁的状态,锁的特性,锁的设计 有各式各样的锁…

乐观锁/悲观锁

从并发同步的角度来看,锁 在宏观上分为两种,乐观锁,悲观锁.

synchronized 是悲观锁。

在 JDK1.5 中新增 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的.相对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现.所以 J.U.C 在性能上有了很大的提升.

juc 是乐观锁

再看一下 atomic 类

悲观锁

假设最坏情况,每次都会修改数据,故没有线程访问就上锁.

“操作前先上锁.”

用于

传统关系型数据库,行锁,表锁,读锁,写锁…

Java, Synchronized,ReentrantLock

乐观锁

取数据,不上锁

更新数据,会检查有没有被别人更新.

用于

多读类型应用 –> 提高吞吐量

如 数据库 –> write_condition 机制

Java –> java.util.concurrent.atomic 包下的原子类 –> 用 CAS 实现

实现方式

版本号机制

就是加个版本号,每次修改操作,版本号相应变动.(如自增)

别的线程执行修改时,发现版本号不一致,就修改失败…

CAS 算法

CAS 算法可以单独提出来

即 compare and swap (比较与交换)

是一种 无锁算法 –> 不使用 锁 实现多线程之间的变量同步.

在没有线程被阻塞的情况下实现变量的同步 也称 –> 非阻塞同步(Non-blocking Synchronization)

涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

我认为 V 的值应该为 A,如果是,那么将 V 的值更新为 B,否则不修改并告诉 V 的值实际为多少.

1
2
3
4
5
6
7
8
9
int compare_and_swap (int* reg, int oldval, int newval) 
{
ATOMIC();
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return old_reg_val;
}

一般情况是是一个 自旋操作 即不断地重试…

缺点

  • ABA 问题

    如果一个值原来为 A 变化为 B 又变化为 A

    看起来没变过 实际变了

    解决办法 : 用版本号

  • 循环时间长开销大

    增加CPU的开销

  • 只能保证一个共享变量的原子操作

    为什么?

    硬件实现的

    因为它本身就只是一个锁住总线的原子交换操作

    两个 CAS 操作之间并不能保证没有重入现象

    只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

自旋锁

广泛应用的底层同步机制

  • 许多情况下,共享资源的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出 CPU.
  • 不过若锁被其它线程长时间占用,会带来许多性能上的开销.
  • 因此自旋应当设置一个超时判断.

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

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting

java.util.concurrent.atomic 包下的原子类就是自旋锁.CAS 就是自旋锁的一种是实现方式.

独享锁/共享锁

独享锁:是指该锁一次只能被一个线程所持有。

共享锁:是指该锁可被多个线程所持有。

Java ReentrantLock 是独享锁.

对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁.

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的.

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享.

互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

  • 互斥锁在Java中的具体实现就是 ReentrantLock
  • 读写锁在Java中的具体实现就是 ReadWriteLock

读写锁将对一个资源的访问分成了2个锁,一个读锁,一个写锁.

同一时刻允许多个线程进行读访问.

但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.

公平锁/非公平锁

公平锁

公平锁即尽量以请求锁的顺序来获取锁.

比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁.

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的.这样就可能导致某个或者一些线程永远获取不到锁.

在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序.

而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁.

区分

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

公平锁即尽量以请求锁的顺序来获取锁.

比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁.

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的.这样就可能导致某个或者一些线程永远获取不到锁.

在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序.

而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁.

可重入锁

具备可重入性就是可重入锁.

可重入性上面也提到了,即 支持一个线程对资源的重复加锁.

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

按上面提到的,synchronized 和 ReentrantLock 都是可重入锁.

不过 synchronized 是隐式的,获取了锁,直接再用就是了.

但 ReentrantLock 是 : 调用了 lock() 方法获取到锁的线程,可以再次调用 lock() 获取锁而不被阻塞.

读写锁

读写锁将对一个资源的访问分成了2个锁,一个读锁,一个写锁.

同一时刻允许多个线程进行读访问.

但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.

可中断锁

顾名思义,就是可以中断的锁.

Java 中,synchronized 就是不可中断锁.

Lock 是可中断锁.使用 ockInterruptibly().

偏向锁/轻量级锁/重量级锁

指锁的状态

针对 Synchronized

分段锁

一种锁的设计.

把一块数据分成多个段,每个段用一把锁维护.

jdk 1.8 前 ConcurrentHashMap 所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问.

synchronized 升级后的三种锁

来自 : https://www.cnblogs.com/wade-luffy/p/5969418.html

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间,锁占用时间很短
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长

具体实现是通过操作 Java 对象头里的 Mark Word…

下面抄自上面链接,没有深入理解…

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。

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

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

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁

重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

并发编程的三个概念

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

即程序执行的顺序按照代码的先后顺序执行。

这个需要与 指令重排序 联系…

指令重排序

处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

Java 与 CAS

CAS 是乐观锁的一种实现机制

Compare And Swap的缩写,翻译过来就是比较并替换

Java 哪里用到了 CAS ?

atomic 系列类

Lock 系列

jdk1.6 以上的版本,synchronized 转变为重量级锁之前,也会采用 cas 机制.

JVM 对锁的优化

锁消除

JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁.

比如在没有竞争的情况下调用 StringBuffer 的 append(),JVM 会消除 append() 方法上的 synchronized 的影响.

锁粗化

通过扩大加锁范围,避免反复加锁解锁.

其他

  • AQS
  • happens-before
  • 继续深入
  • 源码
  • 与操作系统联系