在不一样线程中,对同一变量、方法或代码块进行同步访问html
咱们经过一个例子了解锁的不一样实现,开启100个线程对同一int
变量进行++
操做1000次,在这个过程当中如何对这个变量进行同步java
未同步代码:算法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** * \* Created with IntelliJ IDEA. * \* User: guohezuzi * \* Date: 2018-04-30 * \* Time: 上午11:26 * \* Description:本身编写的多线程的栗子(多个线程添加元素到数组中) * \ * * @author guohezuzi */
public class MyExample {
private int count = 0;
class addHundredNum extends Thread {
@Override
public void run() {
//...执行其余操做
for (int i = 0; i < 1000; i++) {
count++;
}
//...执行其余操做
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
// 等待全部addHundredNum线程执行完毕
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
复制代码
经过synchronized(addHundredNum.class)
给当前对象加锁而不是synchronized(this)
给对象实例加锁编程
public class MyExample {
private int count = 0;
class addHundredNum extends Thread {
@Override
public void run() {
//...执行其余操做
synchronized (addHundredNum.class) {
for (int i = 0; i < 1000; i++) {
count++;
}
}
//...执行其余操做
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
复制代码
synchronized关键字通过编译以后,会在同步块的先后分别造成monitorenter和monitorexit这两个字节码指令,这两个字节码都须要一个reference类型的参数来指明要锁定和解锁的对象。若是Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;若是没有明确指定,那就根据synchronized修饰的是实例方法仍是类方法,去取对应的对象实例或Class对象来做为锁对象。 根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。数组
在JDK1.6以前,使用sysnchronized同步时,若是要挂起或者唤醒一个线程,都须要操做系统帮忙完成,而操做系统实现线程之间的切换时须要从用户态转换到内核态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高缓存
JDK1.6以后,JVM对sysnchronized进行了大量优化,从原来的重量级锁到如今的锁的不一样阶段升级 无锁 -> 偏向锁 -> 轻量级锁及自旋锁 -> 重量级锁安全
偏向锁多线程
当进行同步时,偏向于第一个得到它的线程,若是在接下来的执行中,该锁没有被其余线程获取,那么持有偏向锁的线程就不须要进行同步并发
可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,此时,偏向锁会升级为轻量级锁ide
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过CAS自旋的形式尝试获取锁,不会阻塞,从而提升性能。
但若是存在锁竞争,除了互斥量开销外,还会额外发生CAS操做,所以在有锁竞争的状况下,轻量级锁比传统的重量级锁更慢!若是锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
自旋锁和自适应自旋锁
轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。让线程自旋的方式等待一段时间
自适应的自旋锁:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定。
锁消除
指的就是虚拟机即便编译器在运行时,若是检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除能够节省毫无心义的请求锁的时间。
锁粗化
若是一系列的连续操做都对同一个对象反复加锁和解锁,会带来不少没必要要的性能消耗,经过对连续操做的一次加锁和解锁(及锁的粗化)来节省时间
经过JDK层面AQS实现的锁,须要咱们经过编程实现,如调用lock()、unlock()
public class MyExample {
private int count = 0;
private final Lock lock = new ReentrantLock();
class addHundredNum extends Thread {
@Override
public void run() {
lock.lock();
try {
for (int i = 0; i < 1000; i++) {
count++;
}
} finally {
lock.unlock();
}
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
复制代码
AQS详解参考:JAVA多线程 - AQS详解
经过使用原子类的CAS方法来实现
public class MyExample {
private AtomicInteger count = new AtomicInteger(0);
class addHundredNum extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.getAndAdd(1);
}
}
}
public void test() throws InterruptedException {
addHundredNum[] addHundredNums = new addHundredNum[100];
for (int i = 0; i < addHundredNums.length; i++) {
addHundredNums[i] = new addHundredNum();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.start();
}
for (addHundredNum addHundredNum : addHundredNums) {
addHundredNum.join();
}
}
public static void main(String[] args) throws Exception {
MyExample example = new MyExample();
example.test();
System.out.println(example.count);
}
}
复制代码
JDK8可使用新增LongAdder类实现,该类自己会分红多个区域,多线程写入时,写入对应区域,读取会将整个区域统计输入。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。
CAS算法涉及到三个操做数:
当且仅当 V 的值等于 A 时,CAS经过原子方式用新值B来更新V的值(“比较+更新”总体是一个原子操做),不然不会执行任何操做。通常状况下,“更新”是一个不断重试的操做。
volatile关键字使用时,只能做用于变量,且并不能保证不一样线程中的同步,故没法实现上面的同步的例子,接下来咱们来介绍一下volatile
关键字的做用:
保证不一样线程中变量的可见性
volatile英译易挥发的,表示修饰的变量是不稳定的,易改变,故采用volatile修饰后,会将变量放到主内存中,不会放到每一个线程的cpu高速缓存后在读取,而是直接所用线程都经过到主内存去读取,以保证变量在每一个线程的可见性。
然而,这并不意味着变量的线程安全,不一样线程cpu进行运算存在时间差,如当多个线程同时对该变量进行++
操做时,可能其中一个线程读取时变量值为1,这时另一个线程也读取变量值为1,第一个线程cpu进行+1
操做运行完毕并已经写回内存,而另外一个线程cpu才进行+1操做运算并写入内存,此时一个线程的结果被覆盖,致使线程不安全。
防止新建对象的重排序现象
当变量采用volatile修饰后,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。如保守策略的JMM内存屏障插入策略:
在每一个volatile写操做的前面插入一个StoreStore屏障。
在每一个volatile写操做的后面插入一个StoreLoad屏障。
在每一个volatile读操做的后面插入一个LoadLoad屏障。
在每一个volatile读操做的后面插入一个LoadStore屏障。
具体例子可参考文章双重校验锁实现的单例模式中的volatile关键字的做用
《深刻理解Java虚拟机:JVM高级特性与最佳实践》第十三章