JUC并发编程学习笔记二(共享模型之管控)


1. 共享带来的问题

先来看一段代码:

@Slf4j(topic = "c.Test17")
public class Test17 {

    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    counter++;
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    counter--;
                }
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}",counter);
        }
}

问题分析:
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 将获取到的静态变量i的值加上常量1
putstatic i // 将相加的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 将获取到的静态变量i的值减去常量1
putstatic i // 将相减后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换,其中主存中存放共享变量,工作内存中存放线程私有的数据。线程对共享变量的操作必须在各自的工作内存中进行,首先要从主内存读取到共享变量的值到工作空间,然后在工作空间(硬件对应寄存器)内对读取到的值进行操作,操作完成后将值写回到主内存中。
在这里插入图片描述
如果是单线程以上8 行代码是顺序执行(不会交错)没有问题:
在这里插入图片描述
但多线程下这 8 行代码可能交错运行,出现负数的情况:

  1. 线程2从静态变量i中读取到0
  2. 准备常数1
  3. 将i的值和常数1相减得到-1
  4. 正当线程2要将相减后的值赋值给i时,此时线程2的时间片用完,切换到线程1执行
  5. 线程1从静态变量i中读取到0
  6. 准备常数1
  7. 将i的值和常数1相加得到1
  8. 将相加后的值赋值给静态变量i,线程1执行完成,切换到线程2
  9. 线程2将相减后的值赋值给i,即为-1,将线程1执行的i=1结果覆盖了。

如图所示:
在这里插入图片描述
出现正数的情况:
在这里插入图片描述

2. 临界区 Critical Section

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源。多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。例如,下面代码中的临界区。

static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++;
}
static void decrement() 
// 临界区
{ 
 counter--;
}

3. synchronized 解决方案

3.1 应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

本节使用的是阻塞式的解决方案:synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

3.2 synchronized 代码示例

@Slf4j(topic = "c.Test17")
public class Test17 {
    static final Object room = new Object();
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    counter--;
                }
            }
        }, "t2");

            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}",counter);
        }
}

执行多次后发现结果始终为0。

22:30:06.037 c.Test17 [main] - 0

可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程
    t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

在这里插入图片描述

3.3 面向对象改进

把需要保护的共享变量放入一个类

class Room {
    int value = 0;

    public void increment() {
        synchronized (this) {
            value++;
        }
    }

    public void decrement() {
        synchronized (this) {
            value--;
        }
    }

    public int get() {
        return value;      
    }
}

@Slf4j
public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();//锁对象
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}", room.get());
    }
}

3.4 方法上的 synchronized

将synchronized关键字加在普通方法上表示锁住的范围为整个方法语句,锁对象为当前对象。
将synchronized关键字加在静态方法上表示锁住的范围为整个方法语句,锁对象为当前类的class。

class Test {
    public synchronized void test() {

    }
}
//等价于
class Test {
    public void test() {
        synchronized (this) {

        }
    }
}
class Test {
    public synchronized static void test() {
    }
}
//等价于

class Test {
    public static void test() {
        synchronized (Test.class) {

        }
    }
}

3.5 “线程八锁”

其实就是考察 synchronized 锁住的是哪个对象
情况1:12 或 21

@Slf4j(topic = "c.Number")
class Number {
    public synchronized void a() {
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }

情况2:1s后12,或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number {
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}

    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

@Slf4j(topic = "c.Number")
class Number {
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public void c() {
        log.debug("3");
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
        new Thread(() -> {
            n1.c();
        }).start();
    }
}

情况4:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number {
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }


    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n2.b();
        }).start();
    }
}

情况5:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number {
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

情况6:1s 后12, 或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number {
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

情况7:2 1s 后 1

@Slf4j(topic = "c.Number")
class Number {
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n2.b();
        }).start();
    }
}

情况8:1s 后12, 或 2 1s后 1

@Slf4j(topic = "c.Number")
class Number {
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n2.b();
        }).start();
    }
}

3.5 变量的线程安全分析

3.5.1 成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全

如下代码示例所示,test1是一个静态方法,在里面定义了一个局部变量i,将其赋值10后进行了自增操作。

public static void test1() {
 int i = 10;
 i++;
}

查看字节码文件后发现成员变量i的自增操作只用一步就完成了,而共享变量的自增需要四步操作,需要先从主存中读取,在工作内存中对数据进行操作后再写回主存中。

public static void test1();
 descriptor: ()V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10  //准备常量10
 2: istore_0 //存储到变量i中
 3: iinc 0, 1 //将i自增1,与static类型的i不同
 6: return 
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

如图所示每个线程拥有自己独立的一块栈帧空间,也就独享一个成员变量i,不会出现线程安全的问题。
在这里插入图片描述
先看一个成员变量出现线程不安全的例子:定义了一个成员变量的集合list,在主方法中开启了两个线程,这个线程同时会进行对集合list的add和remove操作。

public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    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);
    }
}
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        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) {
        System.out.println(1);
        list.remove(0);
    }
}

}

测试ThreadUnsafe类,执行结果中有很多次出现了以下报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
	at java.util.ArrayList.remove(ArrayList.java:492)
	at cn.itcast.n4.ThreadUnsafe.method3(TestThreadSafe.java:32)
	at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:23)
	at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:13)
	at java.lang.Thread.run(Thread.java:748)

问题分析:经分析,首先我们通过读取ArrayList的源码得知ArrayList的底层是一个名为elementData且类型为Object的数组每次添加元素时都要先判断数组容量是否足够,如果不够就会扩容。它的add(E e)方法是线程不安全的(没有synchronized修饰),这就意味着同一实例下可以有多个线程同时访问此方法,而size++在基础笔记中讲过是可以分成四个步骤,倘若一个线程在读取到size的值为0后,将e的值赋值给elementData[0],之后在寄存器中进行自增操作,正当要将自增后的值1赋值给size时,这时时间片用完,另一个线程开始执行此方法,线程读取到size的值依旧为0,之后将e的值0赋值给elementData[0],之后在寄存器中进行自增操作,将结果1赋值给size,执行结束,切换到线程1根据程序计数器中的地址继续执行,将自增后的值1赋值给size,最后size的结果还是为1,但其实应该是2个,第二个线程对size的操作被覆盖掉了

  • add(E e)方法源码
    在这里插入图片描述

在调用ArrayList的remove(int index)方法时,因为线程1和线程2都会执行一次remove(0),每次remove时会先判断index 是否大于等于 size,若是就抛出IndexOutOfBoundsExecption的异常。之后将size减1。可以发现线程1执行一次后size就变成0了,线程2执行时index >= size,此时就会报错。

  • remove(int index)方法源码
    在这里插入图片描述
    如图所示,可以看到list对象的实例是放在共享空间堆中的,栈中的引用通过地址指向堆中的实例。
    在这里插入图片描述

3.5.2 局部变量是否线程安全?

局部变量是线程安全的
但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全

若想得到线程安全的结果,可以将list变成成员变量,也就是ThreadSafe的效果。但是成员变量也存在线程安全的问题,代码示例如下:在method3()方法执行又新建了一个线程。

public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i + 1)).start();
        }
    }
}

@Slf4j(topic = "c.Test17")
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
        log.debug("{}", list.size());
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe {
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

执行结果:

23:03:32.046 c.Test17 [Thread2] - 1
23:03:32.046 c.Test17 [Thread1] - 2

可以看到最开始创建的两个线程最终各自List的大小分别是1和2,但按理来说都应该是0。分析如下:因为在method3(ArrayList list)中又开启了一个新线程,所以现在有两个临界区,分别对应list.add(“1”)和list.remove(0),其中ArrayList的add源码中要进行size++操作remove源码中要进行–size操作,这时就会出现线程安全问题,经常会出现size值被覆盖的问题。

3.6 常见线程安全类

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类

这里说它们是线程安全的是指多个线程调用它们同一个实例的某个方法时是线程安全的。
如下代码所示Hashtable是一个线程安全类,每个方法都由synchronize修饰,多个线程同时调用方法时不会出现线程安全问题。

Hashtable table = new Hashtable();
new Thread(()->{
 table.put("key", "value1");
}).start();
new Thread(()->{
 table.put("key", "value2");
}).start();

它们的每个方法是原子的,但注意它们多个方法的组合不是原子的。

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

如图所示,线程一先调用get方法判断是否为null,这时时间片用完转去执行线程二,注意此时线程一还拿着table这个对象锁,线程二执行get方法时尝试获取锁没有成功,进入休眠状态,线程一继续执行,get方法执行完后释放锁并唤醒线程二,这时if语句中就有两个线程同时执行,会出现put覆盖的情况
在这里插入图片描述

3.7 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。首先我们得理解String为什么是不可变的。String内部其实有一个final类型的char数组,数组中存放字符串的每一个字符。当我们想修改字符串时会发现底层的char数组因为是final类型的所以不允许我们将新的实例对象指向数组的引用,实际操作是会创建一个新的字符串对象,并将之前字符串的引用指向新创建的字符串对象。包括调用replace,substring方法都是会新创建一个字符串对象。在这里插入图片描述
实例分析
例1 :可知servlet是单例

public class MyServlet extends HttpServlet {
            // 是否安全?//不是
            Map<String,Object> map = new HashMap<>();
            // 是否安全? //是
            String S1 = "...";
            // 是否安全?//是
            final String S2 = "...";
            // 是否安全?//不是
            Date D1 = new Date();
            // 是否安全? //不是
            final Date D2 = new Date();

            public void doGet(HttpServletRequest request, HttpServletResponse response) {
                // 使用上述变量
            }
        }

例2:

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 int count = 0;

    public void update() {
        // ...
        count++;
    }
}

例3:解决方法,使用环绕通知,即在一个方法里创造一个切面。

@Aspect
@Component
public class MyAspect {
    // 是否安全?//不是
    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 UserServiceImpl implements UserService {
    // 是否安全 //是
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
  
    public void update() throws SQLException {
        private Connection conn = null;
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("", "", "");
        // ...
        conn.close();
    }
}

例5:

public abstract class Test {

    public void bar() {
        // 是否安全
        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();
    }
}

其中 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();
        }
    }

习题1:卖票

@Slf4j(topic = "c.Number")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        Window window = new Window(3000);
        int amount = 0;
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 3000; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    int sell = window.sell(new Random().nextInt(5) + 1);
                    sellCount.add(sell);
                }
            });
            t.start();
            list.add(t);

        }

        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 买出去的票求和
        log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", window.getCount());
    }

}


class Window {
    private int count;

    public Window(int count) {
        this.count = count;
    }

    public int sell(int amount) {
        if (count >= amount) {
            count -= amount;
            return amount;
        }
        return 0;
    }

    public int getCount() {
        return count;
    }

}

执行结果:

17:32:23.279 [main] DEBUG c.Number - selled count:3003
17:32:23.285 [main] DEBUG c.Number - remainder count:0

解决方法:在Window的sell方法上加上synchronize关键字

习题2:卖票

@Slf4j(topic = "c.Number")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }
}

执行结果:

16:49:27.174 [main] DEBUG c.Number - total:1960

解决方法:用synchronize(Account.class)包裹transfer方法内部的代码

4. Monitor概念

4.1 Java对象头

以32位虚拟机为例

  • 一个int类型的变量在32位虚拟机中占4字节(32位)
  • 一个Integer类型的对象在虚拟机中占8字节(对象头)+4字节(示例数据)+4字节(对齐填充(必须是8的倍数,所以补齐4字节))=16字节

普通对象:
Klass Word:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在这里插入图片描述
数组对象:
在这里插入图片描述

其中Mark Word结构为:
age:分代年龄
标记位01表示没有与任何的Monitor关联,一旦获得了锁(执行synchronized),就会找一个Monitor与之关联,之后将标记位置为10,并舍弃hashcode、age和biased_lock,就是重量级锁。
在这里插入图片描述
64位虚拟机的Mark Word
在这里插入图片描述

4.2 Monitor(锁)

Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象(操作系统的概念,在Java中并没有Monitor),如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
当Thread-2要执行代码时,通过在obj对象头中MarkWord记录的指针地址,尝试将obj对象与操作系统提供的Monitor对象相关联。
在这里插入图片描述
因为此时只有一个线程Thread-2,它就直接获取到了锁,将Owner设置为Thread-2。
在这里插入图片描述
此时一个新的线程Thread-1来执行代码,它会先检查obj是否有没有关联锁,发现已经关联了Monitor锁,先看这个锁有没有主人,因为Owner只能有一个主人,而此时Thread-2占用了Owner,Thread-1会通过EntryList(阻塞队列)关联Monitor,进入Blocked状态。在这里插入图片描述
Thread-3执行时和Thread-1一样进入Blocked状态,EntryList以链表的形式存放阻塞的线程。
在这里插入图片描述
Thread-2执行完后释放锁,还原MarkWord的状态(normal状态 之前的hashcode、age都存储在Monitor中)。唤醒阻塞的线程,阻塞队列中的线程会竞争锁(一种复杂的算法)
在这里插入图片描述

  • 刚开始Monitor中Owner为null
  • 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner设置为Thread-2,Monitor中只能有一个Owner
    在Thread-2上锁的过程中,如果THread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList队列,进入 BLOCKED状态
  • Thread-2执行完同步代码块的内容,唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
  • 图中WaitSet中的Thread-0,THread-1是之前获得过锁,但条件不满足进入WAITING状态的线程

在这里插入图片描述
注意:
synchronized必须是进入到同一个对象的Monitor才有上述的效果
不加synchronized的对象不会关联监视器,不遵从以上规则

5. synchronized 原理

static final Object lock = new Object();
    static int counter = 0;

    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }

对应的字节码为

public static void main(java.lang.String[]);
        descriptor:([Ljava/lang/String;)V
        flags:ACC_PUBLIC,ACC_STATIC
        Code:
        stack=2,locals=3,args_size=1
        0:getstatic #2 // <- lock引用 (synchronized开始)//获取锁对象lock的引用地址
        3:dup //将引用复制一份 
        4:astore_1 // lock引用 -> slot 1 //将引用存储到临时变量slot1里 以便以后还原对象头的时候使用
        5:monitorenter // 将 lock对象的MarkWord 置为 Monitor的指针(重量级锁)
        6:getstatic #3 // <- i//获取i的值
        9:iconst_1 // 准备常数 1
        10:iadd // +1
        11:putstatic #3 // -> i
        14:aload_1 // <- lock引用//拿到临时变量slot 1的引用
        15:monitorexit // 通过slot 1将lock对象的MarkWord重置, 唤醒EntryList,还原MarkWord
        16:goto 24
        19:astore_2 // e -> slot 2 //以下是出现异常时执行的代码,将异常对象存储到临时变量slot 2 中
        20:aload_1 // <- lock引用//拿到临时变量slot 1的引用
        21:monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 还原MarkWord
        22:aload_2 // <- slot 2 (e)//拿到临时变量slot 2的引用
        23:athrow // throw e //抛出异常
        24:return
        Exception table:
        from to target type
        6    16     19  any //检测6-16行的代码,如果出现异常就去19行
        19   22     19  any
        LineNumberTable:
        line 8:0
        line 9:6
        line 10:14
        line 11:24
        LocalVariableTable:
        Start Length Slot Name Signature
        0 25 0args[Ljava/lang/String;
        StackMapTable:number_of_entries=2
        frame_type=255 /* full_frame */
        offset_delta=19
        locals=[class "[Ljava/lang/String;",

class java/lang/Object]
        stack=[

class java/lang/Throwable]
        frame_type=250 /* chop */
        offset_delta=4

注意
方法级别的 synchronized 不会在字节码指令中有所体现

6. synchronized 原理进阶

6.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁

    static final Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块 A
            method2();
        }
    }

    public static void method2() {
        synchronized (obj) {
            // 同步块 B
        }
    }

每个线程的栈帧都会包含一个锁记录的结构。当线程0执行到synchronized(obj)代码块时,会现在线程0每个栈帧中创建一个锁记录的对象(JVM层面的),由两部分组成:Object reference(锁对象指针),另一部分由lock record地址和锁标记00组成。
在这里插入图片描述
让object reference指向锁对象,把Lock Record中的数据和锁对象中MarkWord的数据互换。
在这里插入图片描述
如下图所示轻量级锁的结构。
在这里插入图片描述
如果 cas(MarkWord和LockRecord中数据的交换操作) 替换成功,对象头中会存储锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
在这里插入图片描述
如果 cas 失败(锁标记为00),有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
一个线程给同一个对象加锁,叫做synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
会在新的栈帧中创建锁记录,把object referencce指向锁对象,之后进行cas交换,但由于该线程已经交换过了(可以通过lock record地址得知),所以会失败,但还是会创建锁记录,但存储的是null,用来计算重入的数量
在这里插入图片描述
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置当前锁记录,表示重入计数减一
在这里插入图片描述
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明有变的线程尝试cas从而导致轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

6.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

   static Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块
        }
    }

Thread-1进入method1()执行代码时,也会在栈帧中创建Lock Record对象,将Object Reference指向锁对象obj,当尝试将obj对象头中的markw和lock record中的数据进行交换时(cas),因为此时markword中的锁标记位已被置成00,所以会失败
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
在这里插入图片描述
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,同时将锁标记置为10
如图重量级锁结构:
在这里插入图片描述
然后自己进入 Monitor 的 EntryList 进入BLOCKED状态
在这里插入图片描述
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁
流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值