• java并发编程学习二——synchronized


    一、synchronized使用

    1.1 线程安全问题

    • 一个线程是没有问题的
    • 多个线程同时访问一个共享资源
      • 只进行读操作也没问题
      • 在多个线程对共享资源进行读写操作,发生指令交错就会出现问题
    • 最终导致不可预期的结果,称为线程安全问题

    另外还有两个术语:临界区和竞态条件
    1.临界区,一段代码块如果存在对共享资源的读写操作就称为临界区
    2.竞态条件,多线程在临界区执行,由于代码执行序列不同导致结果无法预期,称之为发生了竞态条件

    1.2 synchronized解决线程安全

    语法:
    synchronized(对象){
    //临界区
    }
    示例代码:

    public class Synchronized1 {
    
        public static void main(String[] args) throws InterruptedException {
            Member m = new Member();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    m.increment();
    
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    m.decrement();
                }
            });
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println(m.getCounter());
    
        }
    
    
    }
    
    class Member {
        static int counter;
    
        synchronized void increment() {
            counter++;
        }
        //上下等价
        void increment1() {
            synchronized (this) {
                counter++;
            }
        }
    
        synchronized void decrement() {
            counter--;
        }
        
        synchronized static void decrement1() {
            counter--;
        }
        //上下等价
        static void decrement2() {
            synchronized (Member.class) {
                counter--;
            }
        }
    
        synchronized int getCounter() {
            return counter;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    注意:非静态方法的synchronized锁this对象,静态方法的synchronized锁类对象。线程获取到锁之后,在无外界干扰(wait方法等)的情况下,将临界区代码执行完才会释放锁,即使没有获得CPU时间片。

    1.3 synchronized练习

    门票超卖问题

    public class SellTicket {
        static Random random = new Random();
    
        public static void main(String[] args) throws InterruptedException {
            TicketWindow ticketWindow = new TicketWindow(1000);
            Thread[] threads = new Thread[2000];
            List<Integer> list = new Vector<>();
            for (int i = 0; i < 2000; i++) {
                Thread thread = new Thread(() -> {
                    list.add(ticketWindow.sell(random.nextInt(5) + 1));
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                thread.start();
                threads[i] = thread;
            }
    
            for (Thread thread : threads) {
                thread.join();
            }
    
            System.out.println(list.stream().mapToInt(Integer::intValue).sum());
    
            System.out.println(ticketWindow.getCount());
        }
    
    
    }
    
    class TicketWindow {
        private int count;
    
        public TicketWindow(int count) {
            this.count = count;
        }
    
        //获取剩余票数量
        public int getCount() {
            return count;
        }
    
        public synchronized int sell(int amount) {
            if (this.count >= amount) {
                this.count -= amount;
                return amount;
            }
            return 0;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    多共享变量问题

    public class Transfer {
        public static void main(String[] args) throws InterruptedException {
            Acount a = new Acount(1000);
            Acount b = new Acount(1000);
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    a.transfer(b, 3);
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    b.transfer(a, 6);
                }
            });
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            System.out.println(a.getMoney());
            System.out.println(b.getMoney());
        }
    
    }
    
    class Acount {
        private int money;
    
        public Acount(int money) {
            this.money = money;
        }
    
        public int getMoney() {
            return money;
        }
    
        public void setMoney(int money) {
            this.money = money;
        }
    	//方法修饰符添加synchronized无效,只能锁当前对象
        public void transfer(Acount target, int amount) {
            synchronized (Acount.class) {
                if (this.getMoney() >= amount) {
                    this.setMoney(this.getMoney() - amount);
                    target.setMoney(target.getMoney() + amount);
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    二、 synchronized原理

    synchronized锁住的是对象,那么多个线程抢锁,如何知道线程获得了锁呢?线程不会记录自己获得的锁避免遍历线程做判断,锁的状态是记录在对象上面的。线程如果能成功修改对象上锁的状态,就表明该对象获得了锁,可以执行临界区代码。执行完之后再将锁的状态还原,其他线程就可以再次竞争锁。

    2.1 对象头

    对象在内存中的布局分为对象头、实例数据和对齐填充。对象上锁的状态就是记录在对象存储空间的对象头中
    在这里插入图片描述
    对象头又分为两部分,第一部分存储对象自身运行时数据,如锁标记、哈希码、GC分代年龄等,称为Mark Word。另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度,这部分称为Class Word。以32为操作系统为例
    普通对象:
    在这里插入图片描述
    数组对象:
    在这里插入图片描述
    其中Mark Word结构为:
    在这里插入图片描述
    如图,锁状态包括Noramal无锁、Biased偏向锁、Lightweight Locked轻量级锁、Heavyweight Locked 重量级锁、Markd for GC表示被GC标记即将被清除,也是一种无锁状态。

    2.2 Monitor

    Monitor是OS定义的,翻译为监视器或者管程。每个java对象都可以关联一个Monitor对象,当对象被synchronized(obj)加锁(重量级)之后,对象头中的Mark Word会记录指向Monitor对象的指针,即上图中的ptr_to_heavyweight_monitor。synchronized在jdk1.6之后经过优化并不会一开始就使用重量级锁,只有发生竞争升级到重量级锁才会使用Monitor。Monitor结构如下:
    在这里插入图片描述

    • Monitor中的Owner开始为null
    • Thread2执行完synchronized(obj),Monitor将所有者Owner置为Thread2,Monitor中只能有一个Owner
    • 当Thread3、Thread3、Thread4也执行到synchronized(obj)时发现Owner已经指向Thread2,就进入阻塞队列EntryList
    • Thread2执行完同步代码块,唤醒EntryList中的所有线程竞争,竞争是非公平的
    • thread0,thread1是之前已经获得过锁,在调用了obj.wait等方法之后进入等待集合WaitSet

    注意:synchronized必须加在同一个对象上,不加synchronized的对象不会关联监视器,不遵从上述效果。

    2.3 字节码角度

    public class Synchronized {
        static final Object lock = new Object();
        static int counter = 0;
    
        public static void main(String[] args) {
            synchronized (lock) {
                counter++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面这段代码,编译成字节码,再反汇编就得到字节码,Windows系统cmd进入命令行

    javac Synchronized.java
    javap -c Synchronized.class
    
    • 1
    • 2

    得到的字节码指令,加上了中文注释

    public class synchronize.Synchronized {
      static final java.lang.Object lock;
    
      static int counter;
    
      public synchronize.Synchronized();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #2                  // lock引用,synchronized开始
           3: dup
           4: astore_1							// 临时存储lock引用 ->slot1
           5: monitorenter						// lock对象头置为Monitor指针
           6: getstatic     #3                  // 获得counter
           9: iconst_1							// 准备常量 1
          10: iadd								// +1
          11: putstatic     #3                  // 写回counter
          14: aload_1							// 加载lock引用 <-slot1
          15: monitorexit						// 重置lock MarkWord,唤醒EntryList
          16: goto          24					// 正常结束返回
          19: astore_2							// 发生异常,e->slot2
          20: aload_1							// 加载lock引用 <-slot1
          21: monitorexit						// 重置lock MarkWord,唤醒EntryList
          22: aload_2							// 加载e引用 <-slot2
          23: athrow							// 抛出异常
          24: return
        Exception table:
           from    to  target type
               6    16    19   any				// 6-16行发生异常进入19行
              19    22    19   any				// 19-22行发生异常再进入19行,确保释放锁
    
      static {};
        Code:
           0: new           #4                  // class java/lang/Object
           3: dup
           4: invokespecial #1                  // Method java/lang/Object."":()V
           7: putstatic     #2                  // Field lock:Ljava/lang/Object;
          10: iconst_0
          11: putstatic     #3                  // Field counter:I
          14: return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    三、synchronized优化

    3.1 轻量级锁

    执行synchronized(obj) 不会马上使用Monitor实现的重量级锁,在没有发生锁竞争时都是使用轻量级锁。那么什么是轻量级锁呢,轻量级锁是相对于原来使用操作系统互斥量来实现的传统锁而言的,所以才有轻重的区别。下面看下轻量级锁的实现过程
    1.在线程栈帧中创建一个锁记录结构,JVM中称之为Lock Record,内部可以存储锁对象头的Mark Word。Lock Record中有两块区域,一块指向锁对象地址,称之为Object reference。另一块存储Lock Record的地址以及锁标记(00就代表轻量级锁)
    在这里插入图片描述
    2.尝试用CAS操作替换锁对象中的Mark Word,将MarkWord存放到线程栈帧。
    在这里插入图片描述
    3.如果CAS替换成功,代表加锁成功,锁对象的对象头将指向lock record,标记为变为00。如下图所示
    在这里插入图片描述
    4.如果CAS替换失败,有两种情况

    • 如果是其他线程已经获得对象的轻量级锁,则进入锁膨胀
    • 如果是自身线程再次执行synchronized重入锁,就在线程栈帧添加一条Lock Record(只有Object reference,锁记录为null)作为重入记录,重入多少次,解锁时就要释放多少次。
      在这里插入图片描述

    5.退出synchronized代码块时释放锁,遍历所有的Lock Record如果锁记录为null,说明是重入锁,删除Lock Record,重入计数减一。直到最后一个锁记录不为null的Lock Record,使用CAS将对象头Mark Word和锁记录交换回来。

    • CAS成功,则释放锁成功
    • CAS失败,说明其他线程将对象头Mark Word修改,发生了竞争。锁已膨胀成重量级锁,进入重量级锁解锁流程

    3.2 锁膨胀

    锁膨胀或者叫锁升级,指的是在CAS操作尝试加轻量级锁时失败,说明已经有其他线程加上了轻量级锁(发生了竞争),将轻量级锁变为重量级。下面看下膨胀过程
    1.thread1加锁时发现,thread0已经对Objet加了轻量级锁
    在这里插入图片描述
    2.thread1加锁失败,进入锁膨胀

    • thread1为Object对象申请Monitor,对象头中Mark Word指向重量级锁地址
    • Monitor的所有者置为thread0,自己进入entrylist等待
      在这里插入图片描述

    3.thread0解锁时,使用CAS替换对象头Mark Word失败,进入重量级锁解锁流程。将Monitor的Owner置为空,唤醒Entrylist的Blocked线程进行锁竞争。

    3.3 自旋优化

    自旋优化的时机是线程获取轻量级锁失败,在获取重量级锁过程中不会立即阻塞(进入Entrylist),先自旋尝试获取锁。到达临界值后,再阻塞该线程,直到被唤醒。
    自旋成功情况
    在这里插入图片描述
    自旋成功的好处是,自旋过程中线程不放弃CPU时间片,不会发生线程切换,达到提升效率的目的。但是在单核情况下,不放弃CPU其他线程无法执行,也不会释放锁,自旋永远失败。
    自旋失败情况
    在这里插入图片描述
    自旋次数过多就会浪费CPU资源,java6之后自旋次数是自适应的,JVM会分析自旋成功的可能性来决定自旋的次数。

    3.4 偏向锁

    偏向锁是指在轻量级锁发生重入的时候(未发生竞争,竞争就变重量级了),对轻量级锁的一种优化。具体过程是:只有第一次获取锁时使用CAS将线程ID设置到对象头的Mark Word,多次重入时只要线程ID未改变就表示没有竞争,不用进行CAS。之后只要没有竞争,这个锁对象就归该线程所有。
    示例代码:

    public class Biased {
        static Object obj = new Object();
    
        public static void main(String[] args) {
            Biased biased = new Biased();
            biased.m1();
        }
    
        private void m1() {
            synchronized (obj) {
                m2();
            }
        }
    
        private void m2() {
            synchronized (obj) {
                m3();
            }
        }
        private void m3() {
            synchronized (obj) {
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    加锁过程:
    在这里插入图片描述
    看下64为系统对象头的偏向状态,线程ID为54
    在这里插入图片描述
    偏向锁在一些情况下会撤销

    • 调用锁对象的hashCode方法,原因是产生hashcode将覆盖原来存储线程ID的地方,到时偏向锁失效,因此会将偏向状态置为0。另外轻量级锁,hashcode存在栈帧的锁记录;重量级锁,hashcode存在monitor
    • 其他线程使用锁时,将升级为轻量级锁,偏向状态置为0
    • 调用锁对象的wait方法时,会将锁升级为重量级,才能使用wait/notify机制。同时对象的偏向状态也会被撤销,偏向状态置为0

    撤销代表锁膨胀了,如果膨胀成轻量级锁,那么还有机会再次优化为偏向锁。

    • JVM发现有大量对象发生了撤销(偏向状态由可偏向变为不可偏向)
    • 对象个数到达阈值20之后,JVM会将这些对象的偏向状态再次置为1,即可偏向状态。
    • 之后再有线程获取锁,就将对象偏向改线程(在对象头记录ThreadID)。需要注意的是这些都是在没有发生锁竞争的情况做的优化。
  • 相关阅读:
    Allure使用手册
    课题学习(五)----阅读论文《抗差自适应滤波的导向钻具动态姿态测量方法》
    【无标题】
    记录第一次给开源项目提 PR
    MySQL 基本语句
    Elasticsearch搜索引擎该怎么使用,这篇文章彻底讲透(荣耀典藏版)
    源码解析springbatch的job是如何运行的?
    Java学习----线程整理
    RabbitMQ交换机类型
    MongoDB - readConcern
  • 原文地址:https://blog.csdn.net/yx444535180/article/details/126305869