1. 什么是 synchronized
我们将其理解为同步锁, 可以实现共享资源的同步访问, 解决线程并发的安全问题. synchronize 翻译成中文: 同步, 使同步. synchronized: 已同步.
1.1 怎么使用的
修饰实例方法, 作用于当前对象实例加锁, 进入同步代码前要获得当前对象实例的锁
修饰静态方法, 作用于当前类对象加锁, 进入同步代码前要获得当前类对象的锁 . 也就是给当前类加锁, 会作用于类的所有对象实例, 因为静态成员不属于任何一个实例对象, 是类成员( static 表明这是该类的一个静态资源, 不管 new 了多少个对象, 只有一份, 所以对该类的所有对象都加了锁). 所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法, 而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法, 是允许的, 不会发生互斥现象, 因为访问静态 synchronized 方法占用的锁是当前类的锁, 而访问非静态 synchronized 方法占用的锁是当前实例对象锁.
修饰代码块, 指定加锁对象, 对给定对象加锁, 进入同步代码块前要获得给定对象的锁. 和 synchronized 方法一样, synchronized(this)代码块也是锁定当前对象的. synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁. 这里再提一下: synchronized 关键字加到非 static 静态方法上是给对象实例上锁. 另外需要注意的是: 尽量不要使用 synchronized(String a) 因为 JVM 中, 字符串常量池具有缓冲功能!
2. 早期的 synchronized
JDK1.6 之前属于重量级锁, 依赖于操作系统的 Mutex Lock,Java 的线程映射到操作系统的原生线程, 需要操作系统申请互斥量, 操作系统对线程的切换, 需要从用户态切换到内核态, 比较耗时, 效率底下.
3. 对 synchronized 的优化
JDK1.6 之后在 JVM 层面对 synchronized 底层做了很多的优化, 包括偏向锁, 轻量级锁, 自旋锁, 自适应自旋锁, 锁消除, 锁粗化等优化技术.
3.1 偏向锁
目的: 在没有线程竞争的情况下, 减少传统的重量级锁使用操作系统互斥量的开销, 提升性能. 特点:
在没有锁竞争的情况下, 会把整个锁消除
偏向于第一个获取到偏向锁的线程
如果在接下来的执行中偏向锁没有被其他线程获取, 那么拥有该锁的线程就不需要同步
变化: 在锁竞争激烈的场合, 偏向锁失效. 原因是, 在此情况下, 极有可能每次申请锁的线程不是同一个线程, 所以此时不应该使用偏向锁, 否则得不偿失. But, 偏向锁失效后, 并不会立即膨胀为重量级锁, 而是首先升级为轻量级锁. 关于偏向锁的原理可以查看《深入理解 Java 虚拟机: JVM 高级特性与最佳实践》第二版的 13 章第三节锁优化.
3.2 轻量级锁
当偏向锁失效, JVM 不会立即升级为重量级锁, 而是试图使用轻量级锁的优化手段(JDK1.6 之后加入的). 轻量级锁不是为了替代重量级锁, 它的本意是是在没有线程竞争的情况下, 减少传统的重量级锁使用操作系统互斥量的开销, 提升性能.
目的: 和偏向锁一样
特点:
和偏向锁不同, 轻量级锁使用 CAS 操作代替重量级锁.
使用轻量级锁, 不需要申请互斥量.
轻量级锁的加锁和释放锁都是 CAS 操作.
变化: 对于大多数锁来说, 在整个同步周期都不存在竞争, 这来自经验数据. 如果没有竞争, 轻量级锁使用 CAS 操作, 避免了使用互斥锁的开销. 如果存在竞争, 除了互斥锁的开销, 还会有额外的 CAS 操作, 所以如果存在锁竞争, 轻量级锁比重量级锁更慢. 如果竞争激烈轻量级锁会迅速膨胀为重量级锁. 关于轻量级锁的原理可以查看《深入理解 Java 虚拟机: JVM 高级特性与最佳实践》第二版的 13 章第三节锁优化.
3.3 自旋锁和自适应自旋锁
轻量级锁失效后, JVM 避免线程真的在操作系统层面挂起, 还会进行一项成为自旋锁的优化手段. 在 JDK1.6 之前就有这项技术了, 只是他是默认关闭的, 可以通过参数 --XX:+UseSpinning 开启. JDK1.6 之后默认开启. 自旋不能完全替代阻塞, 因为它还要占用处理器的时间. 如果锁被占用的时间短, 那么自旋锁的效果就好; 否则, 反之. 自旋等待的时间必须固定, 如果超过限定的次数, 仍然没有获取到锁, 就挂起线程. 自旋默认 10 次, 可以使用参数 --XX:PreBlockSpin 修改.
3.3.1 为什么会有自旋锁
互斥同步对性能最大的影响是阻塞的实现, 因为线程的挂起和恢复都需要转入内核态去完成(用户态到内核态的转换将会耗费一定的时间). 而一般线程持有锁的时间并不会太长, 如果仅仅为了这一点时间而挂起或恢复线程将会得不偿失. 所以 JVM 团队就想:"能否让后面来的请求获取锁的线程等待一会儿而不被挂起? 看看持有锁的线程是否很快就会释放锁".
目的: 为了减少线程的挂起和恢复, 减少带来的系统开销, 引入自旋锁.
3.3.2 如何实现自旋
为了让一个线程等待, 我们只需要让线程执行一个忙循环(自旋), 这项技术就叫做自旋.
3.3.3 自旋的特点
执行忙循环
自旋次数固定(默认 10 次)
JDK1.6 之前默认关闭, 之后默认打开
效果的好坏依赖于锁被占用的时间的长短
3.3.4 自适应自旋锁
另外, 在 JDK1.6 时候引入了自适应自旋锁. 改进: 自旋次数不是固定的. 根据上次同一个锁的自旋次数和锁的拥有者的状态来确定自旋次数. JVM 变得越来越聪明了. 与自旋锁的区别就是自旋次数不固定.
3.4 锁消除
即使 JVM 正在运行, 如果检测到共享数据不可能存在竞争, 将会执行锁消除操作. 这将会节省毫无意义的请求锁的时间.
3.5 锁粗化
原则上我们写代码, 总是建议将 Synchronized 代码块的作用范围限制的尽量小, 只在共享数据的实际作用域才进行同步, 使需要同步的操作数尽量小, 如果存在竞争, 等待线程也会尽快拿到锁. 大部分情况下, 上面的原则没有问题, 但是如果一些列的连续操作都对同一个对象反复加锁解锁, 会带来很多不必要的性能消耗.
4.Synchronized 和 ReenTrantLock 对比
1 两者都是可重入锁
两者都是可重入锁."可重入锁" 概念是: 自己可以再次获取自己的内部锁. 比如一个线程获得了某个对象的锁, 此时这个对象锁还没有释放, 当其再次想要获取这个对象的锁的时候还是可以获取的, 如果不可锁重入的话, 就会造成死锁. 同一个线程每次获取锁, 锁的计数器都自增 1, 所以要等到锁的计数器下降为 0 时才能释放锁.
2 synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的, 前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化, 但是这些优化都是在虚拟机层面实现的, 并没有直接暴露给我们. ReenTrantLock 是 JDK 层面实现的(也就是 API 层面, 需要 lock() 和 unlock 方法配合 try/finally 语句块来完成), 所以我们可以通过查看它的源代码, 来看它是如何实现的.
3 ReenTrantLock 比 synchronized 增加了一些高级功能
相比 synchronized,ReenTrantLock 增加了一些高级功能. 主要来说主要有三点:
a. 等待可中断; b. 可实现公平锁; c. 可实现选择性通知(锁可以绑定多个条件)
ReenTrantLock 提供了一种能够中断等待锁的线程的机制, 通过 lock.lockInterruptibly()来实现这个机制. 也就是说正在等待的线程可以选择放弃等待, 改为处理其他事情.
ReenTrantLock 可以指定是公平锁还是非公平锁. 而 synchronized 只能是非公平锁. 所谓的公平锁就是先等待的线程先获得锁. ReenTrantLock 默认情况是非公平的, 可以通过 ReenTrantLock 类的
ReentrantLock(boolean fair)
构造方法来制定是否是公平的.
synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待 / 通知机制, ReentrantLock 类当然也可以实现, 但是需要借助于 Condition 接口与 newCondition() 方法. Condition 是 JDK1.5 之后才有的, 它具有很好的灵活性, 比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例 (即对象监视器), 线程对象可以注册在指定的 Condition 中, 从而可以有选择性的进行线程通知, 在调度线程上更加灵活. 在使用 notify/notifyAll() 方法进行通知时, 被通知的线程是由 JVM 选择的, 用 ReentrantLock 类结合 Condition 实例可以实现 "选择性通知" , 这个功能非常重要, 而且是 Condition 接口默认提供的. 而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例, 所有的线程都注册在它一个身上. 如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题, 而 Condition 实例的 signalAll()方法 只会唤醒注册在该 Condition 实例中的所有等待线程.
如果你想使用上述功能, 那么选择 ReenTrantLock 是一个不错的选择.
4 性能已不是选择标准
在 JDK1.6 之前, synchronized 的性能是比 ReenTrantLock 差很多. 具体表示为: synchronized 关键字吞吐量随线程数的增加, 下降得非常严重. 而 ReenTrantLock 基本保持一个比较稳定的水平. 我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地. 后续的技术发展也证明了这一点, 我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化. JDK1.6 之后, synchronized 和 ReenTrantLock 的性能基本是持平了. 所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的! JDK1.6 之后, 性能已经不是选择 synchronized 和 ReenTrantLock 的影响因素了! 而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized, 所以还是提倡在 synchronized 能满足你的需求的情况下, 优先考虑使用 synchronized 关键字来进行同步! 优化后的 synchronized 和 ReenTrantLock 一样, 在很多地方都是用到了 CAS 操作.
来源: https://www.cnblogs.com/ibigboy/p/11415439.html