zoukankan      html  css  js  c++  java
  • 关键字:synchronized

    保证三大特性

    原子性

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

    没有synchronized:

    public class AtomicTest {
        private static int v = 0;
        public static void main(String[] args) {
            new Thread(AtomicTest::add).start();
            new Thread(AtomicTest::add).start();
            new Thread(AtomicTest::add).start();
            new Thread(AtomicTest::add).start();
            new Thread(AtomicTest::add).start();
            while (Thread.activeCount()>2){
    
            }
            System.out.println(v);
        }
        private static void add(){
            for (int i = 0; i < 10000; i++) {
                v++;
            }
        }
    }
    

    image-20210122092408586

    给add方法加上synchronized后,每次执行结果就是50000了

        private static final Object obj = new Object();
        private static void add(){
            synchronized (obj){
                for (int i = 0; i < 10000; i++) {
                    v++;
                }
            }
        }
    

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

    可见性

    没有synchronized:

        private static boolean flag = true;
        public static void main(String[] args) {
            new Thread(()->{
                while (flag){
                }
            }).start();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()-> flag = false).start();
        }
    

    while循环不停止

    加上synchronized:

        private static boolean flag = true;
        private static Object obj = new Object();
        public static void main(String[] args) {
            new Thread(()->{
                while (flag){
                    synchronized (obj){
    
                    }
                }
            }).start();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()-> flag = false).start();
        }
    

    运行2秒后停止,因为synchronized对应的lock原子操作,会刷新线程工作内存中的共享变量在主内存中的最新值。

    有序性

    @JCStressTest
    @Outcome(id={"1","4"},expect = Expect.ACCEPTABLE,desc = "ok")
    @Outcome(id="0",expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
    @State
    public class OrderTest {
        int num = 0;
        boolean ready = false;
    
        @Actor
        public void actor1(I_Result r){
            if(ready){
                r.r1 = num+num;
            }else{
                r.r1 = 1;
            }
        }
    
        @Actor
        public void actor2(I_Result r){
            num = 2;
            ready = true;
        }
    }
    

    image-20210122102934001

    出现返回结果是0的情况只有一种,先执行了actor2中的ready=true,然后actor1继续执行,这时候num=0,所以返回值为0+0等于0。证明了发生重排序

    添加synchronized:

        private final Object obj = new Object();    
        @Actor
        public void actor1(I_Result r){
            synchronized (obj){
                if(ready){
                    r.r1 = num+num;
                }else{
                    r.r1 = 1;
                }
            }
        }
    
        @Actor
        public void actor2(I_Result r){
            synchronized (obj){
                num = 2;
                ready = true;
            }
        }
    

    image-20210122103835777

    synchronized保证有序性原理,我们加上synchronized后,依然会发生重排序,但是我们有同步代码块,以保证只有一个持有锁对象的线程执行同步代码块中的代码,保证有序性。

    synchronized的特性

    可重入特性

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

    public class ReentryTest {
        public static void main(String[] args) {
            new ReentryTestThread().start();
            new ReentryTestThread().start();
        }
    }
    
    class ReentryTestThread extends Thread{
    
        @Override
        public void run() {
            synchronized (ReentryTestThread.class){
                System.out.println(Thread.currentThread().getName()+"------1");
                synchronized (ReentryTestThread.class){
                    System.out.println(Thread.currentThread().getName()+"------2");
                }
            }
        }
    }
    

    image-20210122105055877

    可重入原理:synchronized的锁对象中有一个计数器(recursions)变量会记录线程获得几次锁。在执行完同步代码块时,计数器的数量会-1,直到计数器数量为0时,就释放锁。

    可重入好处:

    • 避免死锁
    • 可以让我们更好的来封装代码

    不可中断特性

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

        private static Object OBJECT = new Object();
        public static void main(String[] args) throws InterruptedException {
            Runnable run= () -> {
                synchronized (OBJECT) {
                    System.out.println("进入同步代码块");
                    try {
                        Thread.sleep(100000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            Thread thread1 = new Thread(run);
            thread1.start();
            Thread.sleep(1000);
            Thread thread2 = new Thread(run);
            thread2.start();
            System.out.println(thread1.getState());
            System.out.println(thread2.getState());
            thread2.interrupt();
            System.out.println(thread1.getState());
            System.out.println(thread2.getState());
        }
    

    image-20210122112416159

    反汇编学习synchronized原理

    public class Demo1 {
    
        private static Object obj = new Object();
    
        public static void main(String[] args) {
            synchronized (obj){
                System.out.println("1");
            }
        }
    
        public synchronized void test(){
            System.out.println("a");
        }
    }
    
    javap -p -v Demo1.class
    

    main方法中:

    image-20210122135446262

    monitorenter

    jvm规范中对于monitorenter描述:

    image-20210122140113491

    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

    objectref必须是reference类型。

    每个对象都与一个监视器相关联。当且仅当监视器有所有者时才锁定监视器。执行monitorenter的线程尝试获取与当前对象关联的监视器的所有权,如下所示:

    如果与关联的monitor的条目计数为零,则线程进入监视器并将其条目计数设置为1。然后线程就是监视器的所有者。

    如果线程已经拥有与objectref关联的monitor,它将重新进入监视器,并增加其条目计数。

    如果另一个线程已经拥有与objectref关联的monitor,则该线程将阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。

    所以monitor才是真正的锁,JVM会创建一个monitor C++对象。

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

    monitorexit

    image-20210122141210143

    objectref必须是reference类型。
    执行monitorexit的线程必须是与objectref引用的实例关联的监视器的所有者。
    线程减少与objectref关联的监视器的条目计数。如果结果是entry count的值为零,则线程将退出监视器,不再是其所有者。其他阻止进入监视器的线程可以尝试这样做。

    总结

    image-20210122141903205

    注意:monitorexit出现了两次,为什么?

    注意下面的exception table 中的 from to target:指的是从6到16中如果出现异常,直接跳转到19号指令。意思是之间如果出现异常了,没有正常monitorexit(正常释放锁),还有一个保底方案,22号指令也是monitorexit释放锁。

    所以:synchronized代码块中出现异常,也会释放锁。

    面试题:synchronized和Lock区别

    • synchronized是关键字,Lock是接口
    • synchronized会自动释放锁,Lock需要手动释放
    • synchronized是不可中断的,Lock可以中断,也可以不中断
    • 通过Lock可以知道线程有没有拿到锁,而synchronized不能
    • synchronized能锁住方法和代码块,而Lock只能锁住代码块
    • Lock可以使用读锁提高多线程读效率
    • synchronized是非公平锁,ReetrantLock可以控制是否是公平锁

    synchronized优化

    Java对象的布局

    在jvm中,对象在内存中的布局分为三块区域:对象头、示例数据和对齐填充。

    image-20210124170456728

    对象头:当前一个线程尝试访问synchronized修饰的代码块时,他首先要获得锁,这个锁是存在锁对象头中的

    偏向锁

    偏向锁是jdk1.6的中澳引进,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

    偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

    image-20210125103931127

    偏向锁的原理

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

    偏向锁的撤销

    1. 偏向锁的撤销动作必须等待全局安全点
    2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
    3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

    偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。

    偏向锁好处

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

    轻量级锁

    轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

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

    轻量级锁加锁过程

    • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word

    image-20210125110819133

    • 拷贝对象头中的Mark Word复制到锁记录中;
    • 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
    • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

    image-20210125110851861

    • 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

    轻量级锁

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

    自旋锁

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

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

    适应性自旋锁

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

    锁消除

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

    锁粗化

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

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

    平时写代码如何对synchronized优化

    减少synchronized范围

    同步代码块中代码尽量短小精悍

    降低锁的粒度

    将一个锁拆分为多个锁提高并发度

    HashTable:锁定整个hash表,一个操作正在执行时,其他操作也同时锁定,效率低下

    image-20210125115443111

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

    image-20210125115422888

    LinkedBlockingQueue:入队出队使用不同的锁,相对于读写只有一把锁效率要高。

    image-20210125115530655

    读写分离

    读取时不加锁,写入和删除时加锁

    ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteSet

  • 相关阅读:
    Eclipse设置打开的默认浏览器
    Java | 源文件
    博客园--个人博客背景设置
    MYSQL | 修改密码
    博客园首秀----Markdown
    Redis@Redis
    网络编程@Socket网络编程
    JVM@JVM基础
    并发编程@Disruptor并发框架
    并发编程@并发编程高级
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/14324497.html
Copyright © 2011-2022 走看看