zoukankan      html  css  js  c++  java
  • 02 Java的synchronized原理与Monitor对象

    1 基本概念

    案例:采用2个无关联的线程对同一变量进行操作,一个进行5000次自增操作,另外一个进行5000次自减操作。

    最终变量的结果是不确定的(2个算数操作的操作指令由于多线程原因会交错在一起)。

    临界区(critical section):对共享资源进行多线程读写操作的代码块。

    竞态条件(Race Condition):多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称之为发生了竞态条件

    Java中如何避免发生竞态条件?

    • 阻塞式解决方案:synchronized, Lock
      • synchronized俗称“对象锁”,采用互斥的方式使得同一时刻最多只有一个线程能拥有这个“对象锁”。
    • 非阻塞式:原子变量

    2 Java中synchronized的使用与理解

    synchronized (对象){         // 申请对象锁
               临界区;
    }
    

    2-1 基本的使用

    package chapter3;
    //这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
    import lombok.extern.slf4j.Slf4j;
    @Slf4j(topic = "c.Test1")
    public class Test1 {
        static int counter = 0;
        static Object lock = new Object();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(()->{
                for(int i = 0;i < 5000;++i){
                    synchronized (lock){        // 申请对象锁
                        counter++;
                    }
                }
            },"t1");
    
            Thread t2 = new Thread(()->{
                for(int i = 0;i < 5000;++i){
                    synchronized (lock){       // 申请对象锁
                        counter--;
                    }
                }
            },"t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.warn("{}",counter);        // 通过synchronized实现了对共享变量的互斥操作
        }
    }
    

    结果

    [main] WARN c.Test1 - 0
    

    总结:Java中使用synchronized以对象锁的形式保证了临界区的原子性,避免竞态条件的发生。

    上面代码引申:

    • 代码中2次synchronized必须是同一对象

    • 代码中仅仅进行一次synchronized无法保证竞态条件不发生。

    对共享变量进行封装:

    package chapter3;
    //这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
    import lombok.extern.slf4j.Slf4j;
    @Slf4j(topic = "c.Test1")
    public class Test2 {
        static Room room = new Room();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(()->{
                for(int i = 0;i < 5000;++i){
                    synchronized (room){        // 申请对象锁
                        room.increment();
                    }
                }
            },"t1");
    
            Thread t2 = new Thread(()->{
                for(int i = 0;i < 5000;++i){
                    synchronized (room){       // 申请对象锁
                        room.decrement();
                    }
                }
            },"t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.warn("{}",room.getCounter());   // 通过synchronized实现了对共享变量的互斥操作
        }
    }
    
    class Room{
        private static int counter = 0;
        public void increment(){
            synchronized (this){
                counter++;
            }
        }
    
        public void decrement(){
            synchronized (this){
                counter--;
            }
        }
    
        public int getCounter(){
            synchronized (this){
                return counter;
            }
        }
    }
    

    2-2 方法上的synchronized

    2种等价写法:

    class Test{
          public void test(){
              synchronized (this){     // this表示当前类的实例,也叫做qualified this
                  counter++;
              }
          }
    }
    等价于
    class Test{
          public synchronized void test(){
              counter++;
          }
    }
      
    
    //静态方法
    class Test{
          public static void test(){
              synchronized (Test.class){
                  counter++;
              }
          }
    }
    等价于
    class Test{
          public synchronized static void test(){
              counter++;
          }
    }
    

    静态方法的synchronized与普通成员方法synchronized的区别:

    • 静态方法上锁的是这个class。
    • 普通成员方法,锁的是该对象的实例this。
    • 一个class可以多个this实例

    2-3 变量的线程安全分析

    线程安全:多个线程执行同一段代码,所得到的最终结果是否符合预期。

    局部变量:
    • 局部变量是线程安全的
      • 实例:栈帧中每一个frame存储的变量都是相互独立的。
    • 局部变量引用的对象未必:
      • 线程安全的判断依旧:引用的对象是否脱离方法的作用范围
    静态变量:
    • 静态变量没有被多个线程共享,或者被多个线程共享但只进行读操作,那么该静态变量就是线程安全的。
    实例1:局部变量引用带来的线程不安全
    package chapter3;
    //这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
    import lombok.extern.slf4j.Slf4j;
    import java.util.ArrayList;
    @Slf4j(topic = "c.Test4")
    public class Test4 {
        static final int LOOP_NUMBER = 200;
        static final int THREAD_NUMBER = 2;
        public static void main(String[] args){
            ThreadUnsafeExample tmp = new ThreadUnsafeExample();
            for(int i = 0;i < THREAD_NUMBER;++i){
                    new Thread(()->{
                        tmp.method1(LOOP_NUMBER);
                    },"Thread"+i).start();
            }
        }
    }
    
    // 这里定义了一个线程不安全的类
    class ThreadUnsafeExample{
        ArrayList<String> list = new ArrayList<>();
        public void method1(int loopnumber){
            for(int i = 0;i < loopnumber;++i){
                method2();
                method3();
            }
        }
        private void method2(){
            list.add("1");
        }
    
        private void method3(){
            list.remove(0);
        }
    
    }
    

    运行结果:

    Exception in thread "Thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
    	at java.util.ArrayList.remove(ArrayList.java:492)
    	at chapter3.ThreadUnsafeExample.method3(Test4.java:35)
    	at chapter3.ThreadUnsafeExample.method1(Test4.java:27)
    	at chapter3.Test4.lambda$main$0(Test4.java:15)
    	at java.lang.Thread.run(Thread.java:748)
    

    分析:

    • 多个线程通过成员变量共享了堆中的list对象。
    实例2:局部变量的引用暴露带来的线程不安全
    package chapter3;
    //这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    
    @Slf4j(topic = "c.Test5")
    public class Test5 {
        static final int LOOP_NUMBER = 10000;
        static final int THREAD_NUMBER = 2;
        public static void main(String[] args){
            ThreadSafeExampelSubclass tmp = new ThreadSafeExampelSubclass();
            for(int i = 0;i < THREAD_NUMBER;++i){
                    new Thread(()->{
                        tmp.method1(LOOP_NUMBER);
                    },"Thread"+i).start();
            }
        }
    }
    
    class ThreadsafeExample{
        public void method1(int loopnumber){
            ArrayList<String> list = new ArrayList<>();    //方法中new了一个对象,每个线程调用该方法都会new一个对象,因此不存在线程之间共享的成员,所以是安全的。
            for(int i = 0;i < loopnumber;++i){
                method2(list);
                method3(list);
            }
        }
        public void method2(ArrayList<String> list){
            list.add("1");
        }
        public void method3(ArrayList<String> list){
            list.remove(0);
        }
    }
    
    class ThreadSafeExampelSubclass extends ThreadsafeExample{
        @Override
        public void method3(ArrayList<String> list){     // 方法内部创建的对象的引用通过继承被暴露了
            new Thread(()->{
                list.remove(0);
            }).start();
        }
    }
    

    运行结果会出现2种:

    • 没有任何问题,程序正常退出(循环次数比较小的情况下)
    • 出现如下错误:
    Exception in thread "Thread-1446" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
    	at java.util.ArrayList.remove(ArrayList.java:492)
    	at chapter3.ThreadSafeExampelSubclass.lambda$method3$0(Test5.java:41)
    	at java.lang.Thread.run(Thread.java:748)
    Process finished with exit code 0
    

    分析:由于父类使用public修饰list的操作方法,因此对于list的引用被暴露给子类。

    • 子类通过重载将局部变量引用的对象被多个线程共享,引发问题

    线程安全问题实际挺难发现的可以通过一些良好的编程习惯避免。

    通过private,final等关键词保证安全,遵循面向对象编程的开闭原则的闭。

    class ThreadsafeExample{
        public final void method(int loopnumber){
            ArrayList<String> list = new ArrayList<>();
            for(int i = 0;i < loopnumber;++i){
                method2(list);
                method3(list);
            }
        }
        private void method2(ArrayList<String> list){
            list.add("1");
        }
        private void method3(ArrayList<String> list){
            list.remove(0);
        }
    }
    

    2-4 常用的线程安全类

    基本理解

    Java中常用的线程安全类:String, Integer, StringBuffer, Random, Vector, HashTable, java.util.concurrent(juc包)

    线程安全类的理解:多个线程调用同一实例的某个方法时,是线程安全的。

    • 可以理解为线程安全的类的方法是原子的(查看源码会发现有synchronized)
    • 注意多个方法的组合未必是原子的。
    HashTable table = new HashTable();
    //线程1,线程2都会执行的代码
    if(table.get("key") == null){
    	table.put("key",value);
    }
    

    分析: 虽然HashTable是线程安全的,但是上面的代码并不是线程安全的,在实际调度时可以出现下面的情况:

    线程1.get   -->  线程2.get  --> 线程1.put  ---> 线程2.put
    

    即无法保证同一线程中get与put同时执行。想要保证可以另外synchronized。

    不可变类的线程安全

    包括:String, Integer

    由于不可变性,所以这个类别是线程安全的。

    String中replace,substring方法如何保证线程安全?

    这些方法并没有改变原有的字符串,而是直接创建了一个新的字符串。

    实例:String中substring源码(最后return是一个新的实例)

        public String substring(int beginIndex, int endIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            if (endIndex > value.length) {
                throw new StringIndexOutOfBoundsException(endIndex);
            }
            int subLen = endIndex - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return ((beginIndex == 0) && (endIndex == value.length)) ? this
                    : new String(value, beginIndex, subLen);
        }
    

    2-5 线程安全分析实例(重点)

    案例1

    public class MyServlet extends HttpServlet {
        // 是否安全?  不是线程安全的,可以用线程安全的类HashMap去替代。
        Map<String,Object> map = new HashMap<>();
        // 是否安全?  线程安全,String是不可变类
        String S1 = "...";
        // 是否安全?  线程安全,String是不可变类
        final String S2 = "...";
        // 是否安全?  线程不安全,Data()不是线程安全类,其成员可能会引发安全问题。
        Date D1 = new Date();
        // 是否安全?  线程不安全,利用同上 
        final Date D2 = new Date();
        public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    	}
    }
    

    Servlet是运行在tomcat环境下,只有一个实例,所以servlet必定会被tomcat多个线程共享使用

    重点:分析成员变量在多线程环境下的安全性。


    案例2

    public class MyServlet extends HttpServlet {
        // 是否安全?  不是线程安全的,成员变量count并不安全,UserServiceImpl实例受Servlet限制一般也只有
        // 一个。
        private UserService userService = new UserServiceImpl();
        public void doGet(HttpServletRequest request, HttpServletResponse response) {
            userService.update(...);
    	}
    }
    
    public class UserServiceImpl implements UserService {
        // 记录调用次数
        private int count = 0;
        public void update() {
            // ...
            count++;
        }
    }
    

    案例3

    这里利用AOP监测程序运行时间,可以采用环绕通知保证线程安全。

    @Aspect
    @Component
    public class MyAspect {
    	// 是否安全? 不是线程安全的,变量start可以被同一实例的多个线程调用共享
        private long start = 0L; 
        @Before("execution(* *(..))")
        public void before() {
            start = System.nanoTime();
        }
        @After("execution(* *(..))")
        public void after() {
            long end = System.nanoTime();
            System.out.println("cost time:" + (end-start));
        }
    }
    

    案例4

    public class MyServlet extends HttpServlet {
    	// 是否安全 是线程安全的
        private UserService userService = new UserServiceImpl();
    	public void doGet(HttpServletRequest request, HttpServletResponse response) {
    		userService.update(...);
    	}
    }
    
    public class UserServiceImpl implements UserService {
    	// 是否安全  是线程安全的,没有对成员的修改操作
    	private UserDao userDao = new UserDaoImpl();
    	public void update() {
    		userDao.update();
    	}
    }
    
    public class UserDaoImpl implements UserDao {
    	public void update() {
            String sql = "update user set password = ? where username = ?";
                // 是否安全 是线程安全,每个新的线程都会新建一个connection
            try (Connection conn = DriverManager.getConnection("","","")){
                // ...
            } catch (Exception e) {
                // ...
            }
    	}
    }
    

    案例5

    public class MyServlet extends HttpServlet {
        // 是否安全   不安全
        private UserService userService = new UserServiceImpl();
    	public void doGet(HttpServletRequest request, HttpServletResponse response) {
    		userService.update(...);
    	}
    }
    
    public class UserServiceImpl implements UserService {
        // 是否安全   不安全
        private UserDao userDao = new UserDaoImpl();
        public void update() {
            userDao.update();
        }
    }
    
    public class UserDaoImpl implements UserDao {
        // 是否安全 , 不是线程安全的,成员变量conn不安全。被多个线程共享
        // 需要将conn设为局部变量
        private Connection conn = null;
        public void update() throws SQLException {
            String sql = "update user set password = ? where username = ?";
            conn = DriverManager.getConnection("","","");
            // ...
            conn.close();
    	}
    }
    

    案例6

    public class MyServlet extends HttpServlet {
        // 是否安全  安全
        // UserDao userDao = new UserDaoImpl();确保了线程安全,每有一个新的链接都重新new一个,
        // 但是这种写法不推荐,浪费资源。
    	private UserService userService = new UserServiceImpl();
    	public void doGet(HttpServletRequest request, HttpServletResponse response) {
    		userService.update(...);
    	}
    }
    
    public class UserServiceImpl implements UserService {
    	public void update() {
    		UserDao userDao = new UserDaoImpl();
    		userDao.update();
    	}
    }
    
    public class UserDaoImpl implements UserDao {
        // 是否安全  不是线程安全的,成员变量conn不安全。可以被同一实例多个线程共享
        private Connection = null;
    	public void update() throws SQLException {
            String sql = "update user set password = ? where username = ?";
            conn = DriverManager.getConnection("","","");
    		// ...
    		conn.close();
    	}
    }
    

    案例7:判断对象的引用是否泄露,警惕抽象方法引入的外星方法。

    // 定义了一个抽象类
    public abstract class Test {
        public void bar() {
            // 是否安全     
            // 不安全,
            // 子类对foo方法定义并在foo中启动新的线程访问sdf对象,造成sdf在多个线程中出现共享,sdf并不是
            // 这个案例与之前引用暴露带来的不安全问题如出一辙。
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            foo(sdf);
        }
        public abstract foo(SimpleDateFormat sdf);
        public static void main(String[] args) {
            new Test().bar();
        }
    }
    // 
    public void foo(SimpleDateFormat sdf) {
        String dateStr = "1999-10-11 00:00:00";
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                	sdf.parse(dateStr);
                } catch (ParseException e) {
                	e.printStackTrace();
                }
            }).start();
        }
    }
    

    foo在子类中的定义(这里对变量进行了修改)

    public void foo(SimpleDateFormat sdf) {
        String dateStr = "1999-10-11 00:00:00";
    	for (int i = 0; i < 20; i++) {
            new Thread(() -> {
    		try {
                sdf.parse(dateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
            	}
            }).start();
    	}
    }
    

    案例8: String的源代码中对String类定义为何加上final这个关键词?

    Final用于修饰类、成员变量和成员方法。final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);Final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;Final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。
    
    • final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类)
    • 避免用户定义的String中的子类破坏其原有方法的安全性。
    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    
        /** use serialVersionUID from JDK 1.0.2 for interoperability */
        private static final long serialVersionUID = -6849794470754667710L;
    
        /**
         * Class String is special cased within the Serialization Stream Protocol.
         *
         * A String instance is written into an ObjectOutputStream according to
         * <a href="{@docRoot}/../platform/serialization/spec/output.html">
         * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
         */
        private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    
        /**
         * Initializes a newly created {@code String} object so that it represents
         * an empty character sequence.  Note that use of this constructor is
         * unnecessary since Strings are immutable.
         */
        public String() {
            this.value = "".value;
        }
    
        /**
         * Initializes a newly created {@code String} object so that it represents
         * the same sequence of characters as the argument; in other words, the
         * newly created string is a copy of the argument string. Unless an
         * explicit copy of {@code original} is needed, use of this constructor is
         * unnecessary since Strings are immutable.
         *
         * @param  original
         *         A {@code String}
         */
        public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
    

    2-6 多线程卖票实例分析

    错误并行代码:

    package chapter3.exericse;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import java.util.Vector;
    
    
    @Slf4j(topic = "c.Ticket")
    public class Ticket {
        public static void main(String[] args) throws InterruptedException {
            TicketWindow ticketWindow  = new TicketWindow(10000);
            List<Thread> threadList = new ArrayList<>();  // 用于同步所有线程,让所有线程都结束
            List<Integer> amountList = new Vector<>();   // Vector是线程安全的,可以在多线程环境使用
            for(int i = 0;i < 1000; ++i){
                Thread thread = new Thread(()->{
                    // 加个随机睡眠,确保出现问题
                    try {
                        Thread.sleep(randomAmount());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int tmp = ticketWindow.sell(randomAmount());
                    amountList.add(tmp);
    
    
                });
                threadList.add(thread);
                thread.start();
            }
            // 等待所有线程运行完毕
            for (Thread thread : threadList) {
                thread.join();
            }
            // 统计余票
            log.warn("余票:{}",ticketWindow.getCount());
            //统计实际卖出的票,求和
            log.warn("卖出的票: {}",amountList.stream().mapToInt(i->i).sum());
        }
        // random是线程安全的
        static Random random = new Random();
        public static int randomAmount(){
            //随机范围1-5
            return random.nextInt(5)+1;
        }
    }
    
    // 定义售票窗口,提供查看余票并售票的功能
    // 这个类会在多线程环境下运行
    class TicketWindow{
        // 统计剩余的票数
        private int count;
        public TicketWindow(int count){
            this.count = count;
        }
    
        public int getCount(){
            return count;
        }
    
        // 售票方法,返回售出票的数量
        public int sell(int amount){
            if(this.count >= amount){
                this.count -= amount;
                return amount;
            }else{
                return 0;
            }
        }
    }
    
    

    运行结果:

    [main] WARN c.Ticket - 余票:7033
    [main] WARN c.Ticket - 卖出的票: 2983
    

    总结:

    • 可以看到定义的TicketWindow在多线程环境下出现票数的统计错误。说明这个类是线程不安全的。
    • 多线程问题难以复现:实际运行时发现多次运行有时候票数统计是正确的,有时候不正确,说明多线程问题比较难以排查。

    买票的多线程问题分析:

    可以发现多线程共享的成员有TicketWindow以及Vector对象的实例,Vector用到了add方法的,由于本身就是线程安全类,因此相关部分没有线程安全问题。
    

    ​ 而TicketWindow的sell方法中count在多线程环境下会被修改,相关联的代码就是临界区。因此可以加一个对象锁。修改代码如下所示。

        // 售票方法,返回售出票的数量
        public synchronized int sell(int amount){
            if(this.count >= amount){
                this.count -= amount;
                return amount;
            }else{
                return 0;
            }
        }
    

    2-7 Monitor对象头以及synchronized工作原理(重要)

    Java对象头的概念(32虚拟机情况):

    • 普通对象:object header由mark word和Klass word,Kclass word是一个指针,指向对象所从属的class。
      • mark word中存储了对象丰富的信息,注意mark word有5种状态表示,当给对象加上synchronized后,如果state是Heavyweight locked,此时加锁的对象通过mark word关联monitor对象。

    • 数组对象:对象头除了包含mark word以及Kclass word还有数组长度

    实例:32位虚拟机下,int类型只占用4字节,而Integer占用4+8字节,其中8字节是对象头

    Monitor(管程)的基本概念:

    • 管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
    • Java中的monitor:每个Java对象都可以关联一个monitor对象,如果对一个对象使用synchronized关键字,那个这个对象的对象头的mark word就被设置指向monitor对象的指针。

    上图中原理讲解:

    • 线程2执行synchronized(obj)会检查关联到Monitor对象中的owner为null,将owner设置为自己,每一个Monitor对象只能有一个owner。
    • 线程1和线程3执行到临界区代码后,同样检查Monitor对象中的owner,由于Monitor对象存在owner,所以进入Entrylist (阻塞队列)进行等待。

    Synchronized字节码层面理解

    总结:

    • 字节码第5行(monitor enter)就是代码执行到synchronized那里,然后将对象头中的mark word设置为Monitor指针
    • 字节码第11行(monitor exit)就是临界区代码执行完成,将对象头的的mark work重置,同时唤醒monitor对象中的EntryList,让其他线程进入临界区。
    • 19-23行适用于处理临界区代码出现异常的情况。

    2-8 synchronized进阶工作原理

    Monitor(重量级锁)虽然能够解决不安全问题,但代价有点高(需要为加锁对象关联一个monitor对象),为了降低代价引入了下列机制:

    基本概念:

    • 轻量级锁
    • 偏向锁
      • 批量重刻名:一个类的偏向锁撤销达到20
      • 不可偏向:某个类别被撤销的次数达到一定阀值(代价过高),设置为不可偏向。

    轻量级锁

    • 基本思想:利用线程中栈内存的锁记录结构作为轻量级锁,锁记录中存储锁定对象的mark word

    • 使用场景:对象虽然有多线程访问,但多线程加锁的时间是错开的(没有竞争)

    • 注意点:轻量级锁不需要用户指定,其使用是透明的,使用synchronized关键字。程序优先尝试轻量级锁。

    2-8-1 轻量级锁的加锁过程
    static final Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
        // 同步块 A
        method2();
        }
        }
        public static void method2() {
        synchronized( obj ) {
        // 同步块 B
        }
    }
    

    上面的代码中进行了2次加锁。

    step1:线程0首先在栈帧中创建锁记录对象

    • 锁记录的Object reference指向加锁的对象

    step2: 使用CAS(Compare and Swap)操作替换加锁对象中对象头的mark word,将mark word存储到所记录

    • 替换成功,则加锁对象的mark word的锁记录地址和状态 00 ,表示light weight locked
    • 替换失败,有2种情况;
      • 一种是线程0以外的其他线程拥有这个线程的轻量锁,发生了竞争,此时进入锁膨胀阶段
      • 线程0再次执行synchronized(锁重入,有点类似于函数内部调用另外一个函数),再添加一条 Lock Record 作为重入的计数(栈的结构)
    • step3: 执行完临界区代码
      • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
      • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
        • CAS成功,则解锁成功
        • CAS失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
    2-8-2 锁膨胀的理解:将轻量级锁变为重量级锁(结合2-8-1)

    发生场景实例:当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

    step1: Thread1 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,自己进入 Monitor 的 EntryList 阻塞等待

    step2:当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,必定失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

    2-8-3 自旋优化

    定义:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。(简单的理解为发现其他线程占着坑位,这个线程没有立刻阻塞而是多等了会)

    注意点:

    • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
    • 自旋功能我们无需操作,Java 7 之后不能控制是否开启自旋功能
    2-8-4 偏向锁

    为什么需要偏向锁?

    • 轻量级锁
      • 优点:轻量级别锁通过线程栈帧中的锁记录结构替代重量级锁,不需要关联monitor对象。
      • 缺点:单个线程(没有其他线程与其竞争)使用轻量级锁,在锁重入的时候仍然需要执行CAS操作(栈帧中添加一个新的lock record,见下图,会有资源浪费)。

    偏向锁为了克服轻量级锁的缺点而提出的

    • 锁重入:同一线程多次对同一对象加锁。

    会发生锁重入的代码:

    static final Object obj = new Object();
    public static void m1() {
        synchronized( obj ) {
            // 同步块 A
            m2();
        }
    }
    
    public static void m2() {
        synchronized( obj ) {
            // 同步块 B
            m3();
        }
    }
    
    public static void m3() {
        synchronized( obj ) {
        // 同步块 C
        }
    }
    
    • 偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
      • 注意点:由于第一次CAS将线程ID设置到加锁对象的对象头的mark word中,发生锁重入的后,就不会再额外产生锁记录。

    2-8-5 偏向状态

    偏向状态可以通过对象头的mark work反应出来,观察64位虚拟机的mark word,如下所示:

    总体上有5种状态,可以通过mark word最后2位判断当前对象的状态。

    state 说明
    Normal(正常状态) biased_lock为0表示没有被加偏向锁
    Biased(偏向状态) biased_lock为1表示被加偏向锁,thread用于存储线程id,注意该id时os层面(非jvm)
    Lightweight Locked(轻量级别的锁) ptr_to_lock_record指向加锁线程栈帧中的锁记录
    Heavyweight Locked(重量锁) ptr_to_heavyweight_monitor指向加锁对象所关联的monitor对象

    偏向锁的一些琐碎知识;

    • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
      thread、epoch、age 都为 0 (对象创建后的默认状态是偏向状态)
    • 偏向锁是默认是延迟的,不会在程序启动时立即生效(需要等一段时间,比如几s),如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
    • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
      age 都为 0,第一次用到 hashcode 时才会赋值。
    2-8-6 对象何时会撤销偏向状态(3种情况,待理解补充)
    • 调用对象 hashCode 方法,由于偏向状态无法存储hash值
    • 其他线程使用对象
      • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
    • 调用wait/notify

    参考资料

    并发编程课程


    20210224

  • 相关阅读:
    深入理解系统调用
    基于mykernel 2.0编写一个操作系统内核
    交互式多媒体图书平台的设计与实现
    Ubuntu下VSCode调试环境搭建指南-C语言篇
    码农的自我修养之必备技能 学习笔记
    工程化编程实战callback接口学习笔记
    CentOS7.4网络配置
    R-CNN
    CUDA整理
    CUDA by Example 第四章 julia集
  • 原文地址:https://www.cnblogs.com/kfcuj/p/14439306.html
Copyright © 2011-2022 走看看