深入学习synchronized

 synchronized关键字最主要的三种应用方式如下:

  • 修饰实例方法,锁是当前实例对象,进入同步代码前要获得当前实例的锁;
  • 修饰静态方法,锁是当前类的Class对象,进入同步代码前要获得当前类对象的锁;
  • 修饰代码块,锁是Synchonized括号里配置的对象,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁;

并发编程的三个问题

可见性

概念

可见性(visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值

public class Main{
    //共享变量
    private static boolean flag=true;
    public static void main(String[] args) throws InterruptedException {
        //创建一个线程不断读取共享变量
        new Thread(()->{
            while (Main.flag){
            }
        }).start();

        Thread.sleep(2000);

        //创建一条线程修改共享变量
        new Thread(()->{
            flag=false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
}
//有一些机器上可能会成功暂停,在一些机器上可能会卡死

并发编程的时候,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的新操作

原子性

原子性 :在一次或多次=操作中,要么所有的操作都执行,并且不会受其他因素干扰而中断,要么所有操作都不执行

import java.util.ArrayList;
import java.util.List;

public class Main{
    //共享变量
    public  static int number=0;
    public static void main(String[] args) throws InterruptedException {
        Runnable increment=()->{
         for(int i=0;i<10000;i++){
             number++;
         }
       };
        List<Thread>list=new ArrayList<>();
       for (int i=0;i<5;i++){
           Thread t=new Thread(increment);
           t.start();
           list.add(t);
       }
       for (Thread t:list){
           t.join();//全部执行完之后再去执行主线程
       }
       System.out.println("number"+number);//输出结果一般会小于50000
    }
}

通过将代码反汇编,将会发现,++指令其实是

依次分别是

  • 取到number的值
  • 准备常量1
  • 让number和1相加
  • 赋值

多条语句,无法保证程序的原子性,多个线程执行会互相干扰

总结

并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作

有序性

有序性(Ordering):是指程序员中代码执行的顺序,就是程序最终的运行顺序

为了提高程序的运行效率,java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序

以上代码执行可能会有四种结果,最后一种结果是因为java可能对代码进行重排序

java内存模型

Java Memory molde (java内存模型/jmm),千万不要和java内存结构混淆(jvm的内存划分,栈,堆,方法区)

java内存模型是java虚拟机规范中定义的一种内存模型,java内存模型是标准化的,屏蔽掉了底层不同的计算机的区别.

java内存模型是一套规范,描述了java程序中各种变量(线程共享原则)的访问规则,以及在jvm中将变量存储到内存和从内存中读取变量这样的底层细节

主内存:

主内存是所有线程共享的,都能访问的,所有的共享变量都存储于主内存

访问朱内存当中给定共享变量时候,必须先拷贝一份到工作内存之中,完成操作之后,再将处理完之后的结果,赋回主内存中.

工作内存:

每一个线程都有自己的工作内存,工作内存只存储该线程对共享变量的副本,线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接写主内存中的变量1,不同的线程池之间也不能直接访问,对方工作内存中的变量

作用

java内存模型是一套在多线程共享数据时,对共享数据的可见性,有序性,和原子性的规则和保障

synchronized,volatile

cpu缓存,内存与java内存模型的关系

​ 通过对前面的cpu硬件内存架构,java内存模型以及java多线程的实现原理的了解,我们应该意识到,多线程的执行最终都会映射到硬件处理器上进行执行

​ 但java内存模型和硬件内存架构并不是完全一致,对于硬件内存来说只有寄存器,缓存内存,主内存的概念,并没有工作内存和主内存之分,也就是说java内存模型对内存的划分对硬件的内存并没有任何的影响,因为jmm只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有困难存储到cpu缓存或寄存器中,因此总体上来说 ,java内存模型和计算硬件内存架构是一个相互交互的关系,是一种抽象概念划分与真实物理硬件的交叉.

小结

java内存模型是一套规范,描述了java程序中各种变量(线程共享变量)的访问原则,以及在jvm中将变量存储到内存和内存中读取变量这样的底层细节,java内存模型是对贡献给数据的可见性,有序性,和原子性的规则和保障

主内存与工作内存之间的交互

java内存模型定义了以下8中操作来完成,主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步会主内存之类的细节,虚拟机实现时必须保证下面体积的每一种操作都是原子的,不可再分的.

注意

如果对于一个变量执行lock操作,将会清空工作内存中此变量的值

对于一个变量执行unlock操作之前,必须先把变量同步到主内存之中

主内存与工作内存之间的数据交互过程

lock->read->load->use->assign->store->wrote->unlock

synchronized保证三大特性

synchronized能够保证同一时刻最多只有一个线程执行该代码,以达到保证并发安全的效果

synchronized(锁对象){
    //受保护资源
}

synchronized与原子性

import java.util.ArrayList;
import java.util.List;

public class Main{
    //共享变量
    public  static int number=0;
    private static Object obj=new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable increment=()->{
            for(int i=0;i<10000;i++){
                synchronized (obj){
                    number++;
                }
            }
        };
        List<Thread>list=new ArrayList<>();
        for (int i=0;i<5;i++){
            Thread t=new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t:list){
            t.join();//全部执行完之后再去执行主线程
        }
        System.out.println("number"+number);//5000
    }
}

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块

synchronized可见性

可见性问题的原因是,线程1的工作内存拷贝了主内存变量中的副本,当线程2改变主内存中的值的时候,线程1没有得到同步

以下代码可以解决该问题

public class Main{
    //共享变量
    private static boolean flag=true;
    private static Object obj=new Object();
    public static void main(String[] args) throws InterruptedException {
        //创建一个线程不断读取共享变量
        new Thread(()->{
            while (Main.flag){
                synchronized (obj){
                    
                }
            }
        }).start();

        Thread.sleep(2000);

        //创建一条线程修改共享变量
        new Thread(()->{
            flag=false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
}

synchronized执行的时候会对应lock这个原子操作,lock会让工作中的内存变量去刷新,得到主内存中的最新值.所以程序就能成功停下来(其实直接在while循环中执行打印语句也会停止程序,因为打印语句中也是用了synchronized关键字)

synchronized有序性

as-if-serial语义

as-if-serial语意的意思是:不管编辑器和cpu如何重新排序,必须保证在单线程情况下,程序的执行结果是正确的,以下数据有依赖关系,不能重排序

写后读

int a=1;
int b=a;

写后写

int a=2;
int a=1;

读后写

int a=1;
int b=a;
int a=2;

编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种成排序会改变执行结果.但是如果操作之间不存在数据依赖关系,这些操作就看被编译器和处理器重排序

int a=1;
int b=2;
int c=a+b;

synchronized保证有序性

必须要等红线标注的代码执行结束之后,才能拿走obj对象执行自己的代码

加了synchronized之后,重排序行为依然会发生,但是发生了重排序也没有问题,因为其他线程没有锁,不会与synchronized代码块修饰的线程相互干扰,保证了有序性

synchronized的特性

可重入

一个线程可以多次执行synchronized,重复获取同一把锁.

public class Main{
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }

}
class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (MyThread.class){
            System.out.println(getName()+"进入了同步代码块1");
            synchronized (MyThread.class){
                System.out.println(getName()+"进入了同步代码块2");
            }
        }
    }
}

结果

Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2

同一个线程可以进入多个代码块拿到同一把锁

原理

synchronzized是可重入锁,synchronized的锁有一个计数器(recursions变量)会记录线程获得几次锁,在执行完同步代码块时,计数器的数量会减一,直到计数器的数量为0就释放这个锁

好处

可以避免死锁

可以让我们更好的来封装代码

不可中断

概念

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断

public class Main{
    private static Object obj=new Object();
    public static void main(String[] args) throws InterruptedException {
        //定义一个Runnable
        Runnable run=()->{
            synchronized (obj){
                String name=Thread.currentThread().getName();
                System.out.println(name+"进入了同步代码块");
                //保证不退出同步代码块
                try {
                    Thread.sleep(800000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //先创建一个线程来执行
        Thread t1=new Thread(run);
        t1.start();
        Thread.sleep(1000);//为了让主线程休眠在run之后

        //开启一个线程来执行同步代码块
        Thread t2=new Thread(run);
        t2.start();

        //停止第二个线程
        System.out.println("停止线程前");
        t2.interrupt();
        System.out.println("停止线程后");

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

执行结果

Thread-0进入了同步代码块
停止线程前
停止线程后
TIMED_WAITING
RUNNABLE

小结

synchronized属于不可被中断

Lock的lock方法是不可中断的

Lock的tryLock方法是可中断的

synchronized

monitor

每一个对象都会和一个监视器monitor关联.监视器被占用时会被锁住,其他线程无法获取该monitor.当jvm执行摸个线程的某个方法内部的monitorenter时,它回去尝试获取当前对象的monitor的所有权.过程如下

  • 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数量置为1.当前线程为monitor的owner(所有者)
  • 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  • 若其他线程已经占用monitor所有权,name当前尝试获取monitor的所有权的线程会被阻塞,知道monitor的进入数量变为0,才能重新尝试获取monitor的所有权

synchronized的锁对象会关联一个monitor,这个monitor不释我们主动创建的,是jvm的线程执行到同步代码块,发现锁对象没有monitor就会创建,monitor内部有俩个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程用有锁的次数,当一个线程拥有monitor后,其他线程只能等待

monitorexit

以下是jvm中对于monitorexit的描述

  • 能执行monitorexit指令的线程一定是拥有挡圈对象的monitor的所有权的线程
  • 执行monitorexit时会讲monitor的进入数减1.当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

monitorexit释放锁

monitorexit插入在方法结处和异常处,jvm保证每个monitorenter必须有对应的monitorexit

synchronized在碰到异常的时候会释放锁

锁方法

在synchronized修饰方法时是添加ACC_Synchonized表示,该标识指明了该方法时一个同步方法,JVM通过ACC_synchronized标志来辨别是否声明为同步方法,从而执行线程的调用

synchronized与Lock的区别

1.synchronized是关键字,而lock是一个接口

2.synchronized会自动释放锁,而lock必须手动释放锁

3.synchronized是不可中断的,Lock可以中断也可以不中断

4.通过Lock可以知道线程有没有拿到锁,而synchronized

4.synchronized能锁住方法和代码块,而Lock只能锁住代码块

5.lock可以使用读锁提高多线程效率.

6.synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

jdk1.6锁升级

CAS

概述

CAS的全称是Compare And Swap(比较(相同)再交换).是现代cpu管饭支持的一种内存中共享数据进行操作的一种特殊指令

CAS作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由cpu保证.

CAS可以保证共享变量赋值时的原子操作.CAS操作依赖三个值:内存中的V,旧的预估值,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中

看如下代码

CAS实现无锁并发(AtomicInteger底层用的就是CAS)

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Main{
    //共享变量
    private static AtomicInteger atomicInteger=new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        Runnable increment=()->{
            for(int i=0;i<10000;i++){
                atomicInteger.incrementAndGet();
            }
        };
        List<Thread>list=new ArrayList<>();
        for (int i=0;i<5;i++){
            Thread t=new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t:list){
            t.join();//全部执行完之后再去执行主线程
        }
        System.out.println("number"+atomicInteger.get());//输出结果一般会小于50000
    }
}

以上代码在执行的时候,底部是一个乐观锁的实现,每次进入的时候,都会比较内存的新值,和旧的预估值是否相等,如果不相等就进入下一次循环,重新再拿到预估值和最新值去比较.直到相等才能退出循环,如果相等才能去修改

CAS原理

在刚才使用的AtomicInteger的源码中我们可以看到,其中包含的Unsafe提供了原子操作

其中的value是这样定义的

private volatile int value;

https://www.bilibili.com/video/BV1aJ411V763?p=21

Unsafe类

unsafe类使java拥有了像c语言一样操作内存空间的能力,同时也带来了指针的问题.国度的使用Unsafe类会使得出错的几率变大,因此java官方并不建议使用,官方文档也几乎没有.Unsafe对象不能直接调用,只能通过反射获得.

乐观锁和悲观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这数据就会阻塞.因此synchronized我们也将其称为悲观锁.JDK中的ReentrantLock也是一种悲观锁,性能较差.

乐观锁

总是假设最好的情况,每次去拿数据的时候偶读认为别人不会修改,就算修改了也没关系,再重试即可,所以不会上锁,但是在更新的时候会判断一下在此期间有没有别人去修改这个数据,如果没有人修改则更新,如果有人修改则重试.

CAS这种机制我们也可以将其称之为乐观锁,综合性能叫好

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁别发,适用于竞争不激烈,多核CPU的场景下

1因为没有使用synchronized,所以线程不会陷入阻塞,这是效率替身的因素之一

2但如果竞争激烈,可以想到重试必然频繁发生

小结

CAS原理:CAS需要三个值,内存地址V,旧的预期值B,要修改的新值B,如果内存地址V和旧的预期值A相等就修改成内存地址B

synchronized锁升级过程

高效并发是从JDK1.5到JDK1.6的一个重要改进,实现了各种锁优化技术

偏向锁,轻量级锁,适应性自旋,锁消除,锁粗化等,这些及时都是为了在线程之间更搞笑的共享数据,以及解决竞争问题,从而提高程序的执行效率

无锁->偏向锁->轻量级锁->重量级锁

java对象的布局

在jvm中,对象再内存中的布局分为三块区域:对象头,实例数据和对齐数据

对象头

当一个线程尝试访问synchronized修饰的代码块时,它要先获得锁,name这个锁在哪里呢?是存在锁对象的对象头中的

Mark Word

Mark word用于存储对象自身的运行时数据,入哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏西那个线程持有的锁,偏向锁ID,偏向时间戳等等,占用内存大小与虚拟机长一致.Mark Word对应的类型是markOop.源码位于markOop.hpp

Klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,jvm通过这个指针确定对象是哪个类的实例.该指针的长度为jvm的一个字节大小,即32位,64位的jvm位64位.

如果应用对象过多,使用64位的指针将浪费大量内存,统计而言,64为的jvm将会比32为的jvm消耗50%的内存,为了解决内存可以使用选项-xx:+UseCompressedOops开启指针压缩.(默认是开启的,改成-号就能关闭)开启该选项后,下列指针将压缩至32位

每个class的属性指针(即静态变量)

每个对象的属性指针(即对象变量)

普通对象数组的每个元素指针

当然也不是所有的指针都会压缩,一些特殊类型的指针jvm不会优化,比如指向PerGen的Class对象指针(JDK8中指向元空间的Class对象指针),本地变量,堆栈元素,入参,返回值和null指针等

对象头=mark word+类型指针

在32位系统中Mark Word=4bytes,类型指针=4bytes,对象头=8bytes=64bits;

在64位系统中,Mark Word=8bytes,类型指针=8bytes,对象头=16bytes=128bits

实例数据

类中定义的成员变量

对齐填充

对齐填充并不是必然存在的,也没有什么特别的意义,仅仅起着占位符的作用,由于HostSpotVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话就是对象的大小必须是8字节的整数倍.而对象头正好是8字节的倍数,因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

偏向锁

偏向锁手机JDK1.6中的重要引进,因为HotPot作者经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁.

偏向锁的意思是这个锁会偏向于第一个获取它的线程,会在对象头存储锁偏向的线程ID,用户改线程进入和退出同步代码块时只需要检查是否为偏向锁,锁标志位以及ThreadID即可

不过一旦出现多个线程晶振时,必须撤销偏向锁,所以偏向锁消耗的性能小于之前生下来的CAS原子锁的性能消耗

持有偏向锁的线程以后每次进入这个锁相关的同步块时,新一年级都可以不再进行任何同步操作,偏向锁的效率高.

偏向锁的撤销

1.偏向锁的车小动作必须等待全局安全点

2.暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态

3.撤销偏向锁,回复到无所(标志位为01)或轻量级锁(标志位为00)的状态

偏向锁是在java1.6之后默认启用的,但在应用程序启动几秒之后才能激活,可以使用

-XX:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常处于竞争状态,可以通过XX:-UseBiaseLocking=false参数关闭偏向锁

偏向锁的好处

偏向锁是在只有一个线程执行同步块时,进一步提高性能,适用于一个线程反复获得同一锁的情况.偏向锁可以提高带有同步但无竞争程序的性能

它用样是一个带有效益权衡性质的优化,也就是说,它不一定总是对程序允许有利,如果程序中大多数的锁总是被多个不同线程访问比如线程池,name偏向锁就是多余的

jdk5中默认关闭了偏向锁,jdk6之后默认启用的,但在应用程序启动几秒之后才能激活

小结

当对象第一次被线程获取的时候,虚拟机会把对象中标志位设为01,即偏向模式.同时使用CAS操作把获取到的这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高

轻量级锁

轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的"轻量级"是对相对于使用monitor的传统锁而言的,因此传统所的锁机制称为"重量级锁".需要强调的一点事,轻量级锁并不是用来代替重量级锁的.

引入轻量级锁的目的:在多线程交替执行同步快下,简历避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要代替重量级锁

当关闭了偏向锁功能,或多个线程竞争导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,步骤如下

1.判断当前对象是否处于无所状态(HashCode,0,01)如果是,则jvm手续爱你将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的Mark Word的拷贝,将对象的Mark Word复制到栈帧中的Lock REcord中,将Lock Reocrd中的owner指向当前对象.

2.jvm利用CAS操作尝试将对象的MArk Word更新为Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作.

3.如果失败则判断挡圈对象的Mark Word是否指向当前线程的栈帧,如果是则表示当期对象已经持有当前对象的锁,则直接执行同步代码块;否则说明该锁对象已经被其他线程抢占了,这时,轻量级锁需要膨胀为重量级锁,锁标志位编程10,后面等待的线程会进入阻塞状态

轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  • 取出获取轻量级锁保存在Displaced Mark Word中的数据
  • 用CAS操作取出数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功
  • 如果CAS操作替换失败,说明其他线程尝试获取该锁,则需要轻量级锁需要膨胀升级为重量级锁.

对于轻量级所,其性能替身的一句是"绝大部分的锁,在整个生命周期内都是不会存在竞争的",如果打破这个一句除了互斥的开销外,还要额外的CAS操作,因此在有多线程的情况下,轻量级锁比重量级锁更慢

轻量级锁的好处

在多线程交替执行同步情况下,可以避免轻量级锁引起的性能消耗

自旋锁

前面我们讨论monitor实现锁的时候,知道monitor 会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作, 这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的- -段时间,为了这段时间阻塞,和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”, 但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) ,这项技术就是所谓的自旋锁。

自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启, 在DK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度, 如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数XX: PreBlockSpin来更改。

适应性自旋锁
在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一-次在同一个锁上的自旋时间
及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行
中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次
循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪
费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来
越准确,虚拟机就会变得越来越"聪明”了。

锁消除

锁消除是指虚拟机即时编译器(JIT) 在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争
的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不
会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就
无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会
在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在
Java程序中的普遍程度也许超过了大部分读者的想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结
果,无论是源码字面上还是程序语义上都没有同步。

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进
行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大
部分情况下,.上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作
是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

什么是锁粗化

jvm会探测到一连串细小的操作都是用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可

写代码时对于synchronized

同步代码块中尽量短,减少同步代码块中的执行时间,减少锁的竞争,避免锁的升级

降低synchronized锁的粒度,讲一个所拆分为多个锁提高并发度

Hashtable对增删改查的方法全部加了锁变成了同步操作

ConcurrentHashMap:局部锁定,只锁桶,当对当前元素锁定时,其他元素不锁定

读写分离 读取不加锁,写入的时候加锁

列子:ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

Last modification:May 28, 2020
如果觉得我的文章对你有用,请随意赞赏