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;
    }

}

注意:非静态方法的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;
    }
}

多共享变量问题

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);
            }
        }
    }
}

二、 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++;
        }
    }
}

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

javac Synchronized.java
javap -c Synchronized.class

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

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."<init>":()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."<init>":()V
       7: putstatic     #2                  // Field lock:Ljava/lang/Object;
      10: iconst_0
      11: putstatic     #3                  // Field counter:I
      14: return
}

三、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) {

        }
    }
}

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

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

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

  • JVM发现有大量对象发生了撤销(偏向状态由可偏向变为不可偏向)
  • 对象个数到达阈值20之后,JVM会将这些对象的偏向状态再次置为1,即可偏向状态。
  • 之后再有线程获取锁,就将对象偏向改线程(在对象头记录ThreadID)。需要注意的是这些都是在没有发生锁竞争的情况做的优化。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值