栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

并发编程 synchronized学习总结

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

并发编程 synchronized学习总结

synchronized原理详解

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源,这种资源可能是:对象、变量、文件等,方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象的访问,同步器的本质就是加锁。synchronized内置锁是一种对象锁,可以用来实现对临界资源的同步互斥访问,是可重入锁。

加锁的方式:

1、修饰普通方法(锁住的是当前实例对象)

public class SynchronizedTest {
    public synchronized void test() {
        try {
            log.info(Thread.currentThread().getName() + " 进入了同步方法");
            Thread.sleep(5000);
            log.info(Thread.currentThread().getName() + " 休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SynchronizedTest t1 = new SynchronizedTest();
        SynchronizedTest t2 = new SynchronizedTest();
        new Thread(() -> {
            log.info(Thread.currentThread().getName() + " t1准备进入同步方法");
            t1.test();//用不同的实例对象去调用
        }).start();
        new Thread(() -> {
            log.info(Thread.currentThread().getName() + " t2准备进入同步方法");
            t2.test();//用不同的实例对象去调用
        }).start();
    }
}

不同实例调用输出:

同一实例调用输出:

结论:同一个实例调用会同步、不同实例调用不会同步

2、修饰静态方法(全局锁):

    public synchronized static void test() {
        try {
            log.info(Thread.currentThread().getName() + " 进入了同步方法");
            Thread.sleep(5000);
            log.info(Thread.currentThread().getName() + " 休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


结论:所有调用该静态方法的线程都会实现同步

3、同步代码块传参this(锁住的是当前类的实例对象)

    public void test() {
        synchronized (this) {
            try {
                log.info(Thread.currentThread().getName() + " 进入了同步块");
                Thread.sleep(5000);
                log.info(Thread.currentThread().getName() + " 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

不同实例调用输出:

同一实例调用输出:
结论:同一个实例调用会同步、不同实例调用不会同步
4、同步代码块传参class对象(全局锁)

    public void test() {
        synchronized (SynchronizedTest.class) {
            try {
                log.info(Thread.currentThread().getName() + " 进入了同步块");
                Thread.sleep(5000);
                log.info(Thread.currentThread().getName() + " 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


结论:所有调用该方法的线程都会实现同步
5、同步代码块传参变量对象 (锁住的是变量对象)

public class SynchronizedTest {

    public Integer i;
    public SynchronizedTest(Integer i) {
        this.i = i;
    }
    
    public void test() {
        synchronized (i) {
            try {
                log.info(Thread.currentThread().getName() + " 进入了同步方法");
                Thread.sleep(5000);
                log.info(Thread.currentThread().getName() + " 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        SynchronizedTest t1 = new SynchronizedTest(100);
        SynchronizedTest t2 = new SynchronizedTest(100);
        new Thread(() -> {
            log.info(Thread.currentThread().getName() + " t1准备进入同步方法");
            t1.test();
        }).start();
        new Thread(() -> {
            log.info(Thread.currentThread().getName() + " t2准备进入同步方法");
            t2.test();
        }).start();
    }
}

Integer范围是-128 -> 127,传参都为100时,会引用同一个常量池地址,所以看作是同一个对象,此时输出:

把t2传参改成new Integer(100),会new一个新的对象,所以看作是不同对象,此时输出:

结论:同一个对象调用会同步、不同对象调用不会同步

synchronized底层原理

JDK1.6之前synchronized性能很低,因为对象锁是依赖对象中的Monitor管程对象,Monitor又会依赖操作系统中的Mutex互斥量,Mutex由操作系统来维护。因为JVM运行在操作系统的用户空间上,调用内核空间时,cpu会从用户态切换到内核态,很消耗性能。这时候卡耐基梅隆大学的教授Doug Lea写了一个AQS框架,基于AQS实现了一种ReentrantLock(java语言实现的), 性能远高于当时的synchronized,所以JVM从1.6之后升级了synchronized的实现,在之前的重量级锁之前新增了偏向锁和轻量级锁,大大提升了性能。

synchronized基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步。
Monitor监视器锁
每一个对象都会有一个Monitor对象,synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块内的开始位置与结束位置,Monitor对象以及锁状态都会记录在在每个Java对象的对象头Mark Word中,Synchronized通过读对象头来获取锁。

对象的内存布局
对象头: 比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等,JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但如果是数组对象无法确定数组的大小,所以数组对象还有一块来记录数组长度内存区。
实例数据: 存放类的属性数据信息,包括父类的属性信息。
对齐填充: 虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

Mark Word用于存储对象自身的运行时数据, 如HashCode、GC分代年龄、锁状态标志、线程持有的锁、线程ID等,是实现轻量级锁和偏向锁的关键。
在32位虚拟机中,对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits存储对象HashCode,4Bits存储对象分代年龄,2Bits存储锁标志位,1Bit固定为0表示无锁,对象的存储结构如下表所示:

现在虚拟机基本是64位的,而64位的对象头有点浪费空间,JVM默认会开启指针压缩,基本上也是按32位的形式记录对象头的。

锁升级原理

偏向锁
偏向锁是JDK1.6之后加入的新锁,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了减少同一线程重复获取锁产生的性能代价,引入了偏向锁。在只有一个获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,可以直接获取锁。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果。
如果两个线程竞争锁的话,偏向锁就失效了,因为两个线程竞争极有可能每次申请锁的线程都是不相同的,此时会把偏向锁升级为轻量级锁。

默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁
偏向锁升级成轻量级锁后,Mark Word的结构也会变为轻量级锁的结构。轻量级锁适应的场景是线程交替执行同步块的场景,是不存在锁竞争的,如果存在同一时间访问同一锁的场景,轻量级锁会先升级为自旋锁。
自旋锁
锁竞争时,没有抢到锁的线程会在cpu中挂起,等待持有锁的线程释放锁后进行唤醒。因为OS实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换时间成本相对较高,所以用自旋锁来优化这一性能。自旋锁适用于每个线程持有锁的时间都不会太长的场景,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做空循环判断锁的状态(空循环次数不会太多),如果锁变成了释放状态,就继续抢锁。如果循环结束还没有释放锁,就只能升级为重量级锁了。
锁升级后是不可逆的
锁粗化
如果代码在同一方法中,存在多个同步块去锁同一个对象时,JVM会把代码优化成把这些逻辑代码合并到一个同步块中,减少锁的获取和释放,从而提升效率,如下:

public class SynchronizedTest {
 
    public void test(){
        Object o = new Object();
        //锁同一对象
        synchronized (o){
            int a = 1;
        }
        synchronized (o){
            int b = 2;
        }
        synchronized (o){
            int c = 3;
        }
    } 
}

优化成:

public class SynchronizedTest {
 
    public void test(){
        Object o = new Object();
        //锁同一对象
        synchronized (o){
            int a = 1;
            int b = 2;
            int c = 3;
        }
    } 
}

锁消除
消除锁是虚拟机另外一种锁的优化,Java虚拟机在JIT编译时,会扫描并去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,例如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,是在线程栈帧中独享的,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争,JVM会自动将StringBuffer的锁消除。

public class SynchronizedTest {

    public void add(String str1) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1);
    }

   public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        SynchronizedTest st = new SynchronizedTest();
        for (int i = 0; i < 10000000; i++) {
            st.add("123");
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - startTime) + " ms");
    }
}

开启锁消除,前提是java必须运行在server模式,64位的虚拟机只支持server模式,Server模式是重量级虚拟机,Client模式是轻量级虚拟机。
JVM使用不同参数的不同输出:

-XX:+DoEscapeAnalysis开启逃逸分析; -XX:+EliminateLocks开启锁消除

-XX:+DoEscapeAnalysis开启逃逸分析; -XX:-EliminateLocks关闭锁消除

-XX:+DoEscapeAnalysis关闭逃逸分析; -XX:+EliminateLocks开启锁消除	


结论:锁消除之后对性能提升有很大帮助,并且锁消除是根据逃逸分析的数据支持的。

暑期编程PK赛 得CSDN机械键盘等精美礼品!
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1016549.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号