并发学习记录03共享模型之管程

共享带来的问题

就是多个线程修改同一变量,由于操作不是原子性的,所以可能会出现修改覆盖的问题

一个例子

@Slf4j(topic = "ch.GuanchengTest01")
public class GuanchengTest01 {
    static int counter = 0;

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

可以看到两个线程同时对counter进行操作,但是得到的结果并不一定是0,因为自增和自减并不是原子操作。
i++的字节码:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

i--的字节码:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

如果要完成静态变量的自增和自减,需要在主存和工作内存中进行数据交换。这时候就很有可能出现修改覆盖的情况。比如t1,t2线程同时读取counte = 0,然后分别对counter进行+1,-1操作,在存储counter的时候,就会发生覆盖,要么先存1,后存-1。本来应该的结果是0,这样就是发生了值的覆盖。

临界区

一个程序运行多个线程本身是没问题的
但是如果多个线程访问共享资源,这时候就可能会出现问题。其实多个进程同时读一个共享资源也是没问题的,关键是多个线程如果同时写一个共享资源,这时候要是发生了指令的交错,就会出现问题。

一段代码块内存在对共享资源的多线程读写操作,就称这段代码块为临界区。
比如:

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

竟态条件

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

synchronized

用于互斥

为了避免临界区的竟态条件发生,有多种手段可以达到目的。
阻塞式的解决方案有:synchronized,Lock
非阻塞式的解决方案有:原子变量

synchronized是什么

synchronized是对象锁,它采用互斥的方式让同一个时刻至多只有一个线程能持有对象锁,其他线程如果想获得这个对象锁就会被阻塞住,这样就能保证拥有锁的线程能安全的执行临界区中的代码,不用担心线程上下文切换。

tips:其实synchronized可以完成同步和互斥,但是也是有区别的。互斥是保证同一时刻只有一个线程执行临界区代码,即同一时刻只有一个线程访问共享资源,在它释放锁之后,其他线程才能访问。

同步:由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点。

语法:

synchronized(对象)
{
  临界区;
}

解决上述问题:

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

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

synchronized:
相当于把counter++的多个字节码操作变成了一个原子操作,只有这个原子操作做完之后,t2线程才能获得锁,然后做counter--的操作。

思考:
如果把synchronized(obj)放在for循环外面,如何理解呢?
这就相当于直接把累加counter一万次的操作变成一个原子性的操作了

如果对于t1加synchronized(obj1),对t2加synchronized(obj2),会怎样?
如果对于t1加synchronized(obj1),对t2不加锁,会怎样?
如果是对于同一临界区加锁的话,就得对同一对象加锁,不然起不到加锁的作用,如下就起不到加锁作用了。

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

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

面向对象的加锁改进

//加锁的面向对象改造
@Slf4j(topic = "ch.GuangchengTest03")
public class GuangchengTest03 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            synchronized (room) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (room) {
                room.decrease();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count值:{}",room.getCount());
    }
}

class Room {
    private int count;

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

    public void decrease() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        synchronized (this) {
            return count;
        }
    }
}

方法上的synchronized

以下两种写法是等价的

class Test {
    public synchronized void test() {

    }
}

class Test1 {
    public void test() {
        synchronized (this) {

        }
    }
}

线程的一些题目分析:

public class GuangChengTest05 {
    public static void main(String[] args) {
        Number01 number01 = new Number01();
        //锁住的对象是类对象,所以结果是12或者21,大部分都是12
        new Thread(() -> {
            number01.a();
        }, "t1").start();
        new Thread(() -> {
            number01.b();
        }, "t1").start();
    }
}

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

    public synchronized void b() {
        log.debug("2");
    }
}
public class GuangChengTest06 {
    public static void main(String[] args) {
        Number02 number02 = new Number02();
        //锁住的还是类对象,所以是begin1秒后打印12,或者是2一秒后打印1,前面的更多些
        new Thread(() -> {
            number02.a();
        }, "t1").start();
        new Thread(() -> {
            number02.b();
        }, "t2").start();
    }
}

@Slf4j(topic = "ch.Number02")
class Number02 {
    public synchronized void a() {
        log.debug("begin");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}
public class GuangChengTest07 {
    public static void main(String[] args) {
        Number03 number03 = new Number03();
        new Thread(() -> {
            number03.a();
        }, "t1").start();
        new Thread(() -> {
            number03.b();
        }, "t2").start();
        new Thread(() -> {
            number03.c();
        }, "t3").start();
    }
}
//ab方法上了锁,c方法没上锁,可能的情况3,1秒后12;32,一秒后1;23,一秒后1
@Slf4j(topic = "ch.Number03")
class Number03 {
    public synchronized void a() {
        log.debug("a begin");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

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

    public void c() {
        log.debug("3");
    }
}
public class GuangChengTest08 {
    public static void main(String[] args) {
        Number04 n1 = new Number04();
        Number04 n2 = new Number04();
        //这种情况线程t1锁的是n1实例,线程t2锁的是n2实例,所以结果只可能是2,1秒后打印1
        new Thread(() -> {
            n1.a();
        }, "t1").start();
        new Thread(() -> {
            n2.b();
        }, "t2").start();
    }
}

@Slf4j(topic = "ch.Number04")
class Number04 {
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}
@Slf4j(topic = "ch.Number05")
class Number05 {
    //static方法代表锁的是类对象
    public static synchronized void a() {
        log.debug("begin");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }
    //这个锁的就是this对象
    public synchronized void b() {
        log.debug("2");
    }
}
//由于他们两个方法锁的不是一个对象,所以b先打印2,然后a经过1秒后打印1
public class GuangChengTest09 {
    public static void main(String[] args) {
        Number05 n1 = new Number05();
        new Thread(() -> {
            n1.a();
        }, "t1").start();
        new Thread(() -> {
            n1.b();
        }, "t2").start();
    }
}
@Slf4j(topic = "ch.Number06")
class Number06 {
    //这两个方法,其实锁的都是类对象
    public static synchronized void a() {
        log.debug("begin");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

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

//打印结果1s后打印12,打印2,一秒后打印1
public class GuangChengTest10 {
    public static void main(String[] args) {
//        Number06 number06 = new Number06();
        new Thread(() -> {
//            number06.a();
            Number06.a();
        }, "t1").start();
        new Thread(() -> {
            Number06.b();
        }, "t2").start();
    }
}
@Slf4j(topic = "ch.Number07")
class Number07 {
    public static synchronized void a() {
        log.debug("begin");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }
}
//先输出2,经过1秒后输出1
public class GuangChengTest11 {
    public static void main(String[] args) {
        Number07 n1 = new Number07();
        Number07 n2 = new Number07();
        new Thread(() -> {
            Number07.a();
        }, "t1").start();
        new Thread(() -> {
            n2.b();
        }, "t2").start();
    }
}
@Slf4j(topic = "ch.Number08")
class Number08 {
    //都有static,所以其实两个方法锁的都是类对象
    public static synchronized void a() {
        log.debug("start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("1");
    }

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

//1秒后打印12;打印2,1秒后打印1
public class GuangChengTest12 {
    public static void main(String[] args) {
        Number08 n1 = new Number08();
        Number08 n2 = new Number08();
        new Thread(() -> {
            n1.a();
        }, "t1").start();
        new Thread(() -> {
            n2.b();
        }, "t2").start();
    }
}

变量的线程安全分析

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

局部变量是否线程安全

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

局部变量线程安全分析

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

每个线程调用test1()方法时调用局部变量i,i会在每个线程的栈帧内存中被创建多份,比如说线程1调用test1,操作的就是test1栈帧中的i,因此不存在共享

局部变量的引用

//不安全的情况
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);
    }
}

public class ThreadSafeUnsafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

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


其实就是多个线程同时去修改了共享的成员变量list

线程安全的情况,把list修改为局部变量

@Slf4j(topic = "ch.ThreadSafeTest")
public class ThreadSafeTest {
    public static void main(String[] args) {
        ThreadSafe test = new ThreadSafe();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                test.method1(1000);
            }, "t" + i).start();
        }
    }
}

class ThreadSafe {
    public void method1(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);
    }
}

分析:
list是局部变量,每个线程调用时会创建不同的实例,没有共享
method2和method3中的参数是method1传过来的,所以就是用的method1中的局部变量


import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;

@Slf4j(topic = "ch.ThreadSafeTest")
public class ThreadSafeTest {
    public static void main(String[] args) {
        
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                test.method1(1000);
            }, "t" + i).start();
        }
    }
}

class ThreadSafe {
    public 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) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe {
    //子类覆盖了父类的方法,新建线程去访问了method1中的局部变量,所以多线程调用也是会有风险的
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }, "t1").start();
    }
}

从这个实例也可以看出private和final的作用,可以避免子类继承和修改方法,也体现了对修改关闭的思想

常见的线程安全类

  • String
  • Integer//也包括诸如其他的包装类
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent包下的类

而且,这里说的线程安全,指的是多个线程调用它们同一个实例的某个方法时,是线程安全的,但是如果调用多个方法,多个方法的组合不是原子的,就比如下面的代码就不一定是线程安全的

Hashtable table = new Hashtable();
if(table.get("key") == null) {
  table.put("key",value);
}

这是如果有线程1访问table,最开始key位置的value为空,所以进入判断语句,这时候发生线程切换,线程2访问table,也会有key位置value为空的判断,所以两个线程都会进入判断语句,最后一定会产生值的覆盖。

不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可改变,因此它们的方法都是线程安全的

String的replace,substring方法又是如何保证线程安全呢?
replace,substring并不改变原来字符串,而是new一个新的字符串使用

实例分析

例子1:

public class MyServlet extends HttpServlet {
 // 是否安全?非线程安全的,HashMap不属于线程安全类,多个线程同时访问可能出现问题
 Map<String,Object> map = new HashMap<>();
 // 是否安全?安全,String是线程安全类
 String S1 = "...";
 // 是否安全?安全
 final String S2 = "...";
 // 是否安全?不安全,Date非线程安全类,多线程同时修改可能会出现问题
 Date D1 = new Date();
 // 是否安全?不安全,D2指向的对象是不能修改的,但是对象之中的日期属性是可以被修改的
 final Date D2 = new Date();

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

例子2:

public class MyServlet extends HttpServlet {
 // 是否安全?非线程安全的,servlet只有一个,所以count是共享的,多线程访问可能会出现问题
 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 {
 // 是否安全?由于Spring中对象都是单例的,所以其实这个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 {
 // 是否安全,安全,虽然有成员变量userService,但是成员变量在下面方法中可以看到是不会改变的
 private UserService userService = new UserServiceImpl();

 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 // 是否安全,安全,虽然有成员变量userDao,但是成员变量在下面方法中可以看到是不会改变的
 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 = ?";
 // 是否安全,这个是线程安全的,只有局部变量,多线程调用会在不同的栈帧中产生不同的局部变量,不会出现覆盖修改的问题
 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是一个成员变量,MyServlet只有一个,UserServiceImpl只有一个实例,所以UserDaoImpl 也只有一个实例,多个线程并发访问单一实例中的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 {
 // 是否安全
 private UserService userService = new UserServiceImpl();

 public void doGet(HttpServletRequest request, HttpServletResponse response) {
 userService.update(...);
 }
}
public class UserServiceImpl implements UserService {
 public void update() {
 //userDao是局部变量,每次调用都会新建一个UserDaoImpl 对象,所以不会出现并发问题
 UserDao userDao = new UserDaoImpl();
 userDao.update();
 }
}
public class UserDaoImpl implements UserDao {
 // 是否安全,Connection 是成员变量
 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() {
 // 是否安全,非线程安全的,是抽象类中的公共方法,可能被子类修改,造成线程安全上的问题
 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();
 }
}

卖票练习

@Slf4j(topic = "ch.ExerciseSell")
//分析线程安全问题,先分析哪部分代码是临界区,然后再对临界区加锁
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(1000);
        List<Thread> threadList = new ArrayList<>();
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            }, "t1");
            threadList.add(thread);
            thread.start();
        }
        log.debug("卖出去的门票和:{}", sellCount.stream().mapToInt(Integer::intValue).sum());
        log.debug("余下的票数:{}", ticketWindow.getCount());
    }

    //一个线程安全的随机数生成器
    static Random random = new Random();

    //生成1-5的随机数
    public static int randomAmount() {
        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;
//        }
//    }
    //修改后
    public synchronized int sell(int amount) {
        if (this.count > amount) {
            this.count -= amount;
            //返回卖了几张票
            return amount;
        } else {
            return 0;
        }
    }
}

转账练习

@Slf4j(topic = "ch.ExerciseTransfer")
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 < 2000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            b.transfer(a, randomAmount());
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("总数为:{}", (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);
//        }
//    }
    //加锁改进
    public void transfer(Account target, int amount) {
        synchronized (Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

monitor概念

Java对象头,以32位虚拟机为例
普通对象

数组对象

其中Mark Word结构为

64位MarkWord

monitor的原理

monitor一般被叫做监视器或者管程
每个java对象都可以关联一个monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置为指向Monitor对象的指针Monitor的结构如下:

  • 最开始Monitor中的Owner为null
  • 当Thread2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread2,Monitor中只能有一个Owner
  • 在Thread2上锁的过程中,如果Thread3、Thread4、Thread5也来执行synchronized(obj),就会进入EntryList等待
  • Thread2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的。
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面会学习
  • wait-notify 时会分析

注意:
synchronized(obj)必须是进入同一个对象的monitor才会有上述的效果
不加synchronized的对象不会关联监视器,不遵从上面的规则

synchronized的字节码

java代码

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开始)
 3: dup
 4: astore_1 // lock引用 -> slot 1
 5: monitorenter //  lock对象 MarkWord 置为 Monitor 指针
 6: getstatic #3 // <- i
 9: iconst_1 // 准备常数 1
 10: iadd // +1
 11: putstatic #3 // -> i
 14: aload_1 // <- lock引用
 15: monitorexit //  lock对象 MarkWord 重置, 唤醒 EntryList
 16: goto 24
 19: astore_2 // e -> slot 2
 20: aload_1 // <- lock引用
 21: monitorexit //  lock对象 MarkWord 重置, 唤醒 EntryList
 22: aload_2 // <- slot 2 (e)
 23: athrow // throw e
 24: return
 Exception table:
 from to target type
 6 16 19 any
 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 0 args [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
假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}

创建锁记录对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁对象的Mark Word

让锁记录中的Object reference指向锁对象,并尝试用cas替换object的Mark Word,将Mark Word的值存入锁记录。
CAS:比较并交换,是一种原子操作,同时CAS是一种乐观机制

如果CAS替换成功,对象头中存储了锁记录地址和状态 00,则表示由该线程给对象加锁这时图示如下:

如果CAS失败,有两种情况
如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
如果是自己执行了synchronized锁重入,那么再添加一条Lock Record 作为重入的计数

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将MarkWord的值恢复给对象头。恢复成功,则解锁成功,恢复失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

锁膨胀

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

method1尝试加锁

static Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块
 }
}

当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁

Thread1发现已经加了轻量级锁,这时自己加轻量级锁失败,进入锁膨胀流程
Thread1为Object对象申请Monitor锁,让Object指向重量级锁地址
然后自己进入Monitor的EntryList的阻塞队列

当Thread0退出同步块解锁时,使用cas将MarkWord的值恢复给对象头,由于对象头现在是重量级锁,所以恢复失败。这时就会进入重量级锁解锁流程,即按照monitor地址找到monitor对象,设置owner为null,唤醒EntryList中BLOCKED线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,就是当前线程在发现同步块有重量级锁时,会不断尝试查看同步代码块是否还有锁,如果没有锁了,就直接加锁执行

自旋成功情况:

自旋失败情况:

注意

自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作
Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的MarkWord头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。

例如:

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

使用轻量级锁时就不停的替换:

使用偏向锁就只需要替换一次,然后接下来重入只需要做检查操作:

偏向状态

对象头格式如下:

一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0
偏向锁是默认延迟的,不会再程序启动时立即生效,如果想避免延迟,可以加VM参数 -
XX:BiasedLockingStartupDelay=0来禁用延迟
如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

测试如下:

@Slf4j(topic = "ch.TestBiased01")
public class TestBiased01 {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        Thread.sleep(3000);
        log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}

class Dog {
}

利用代码不同时间创建两个dog类
打印输入如下:

可见偏向锁的创建确实是有延迟的,需要一段时间后创建对象才会有偏向锁

public static void testBiased02() throws InterruptedException {
        Dog d = new Dog();
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }

执行测试2,观察标记锁的位置,发现锁标记从01变成00再变成01,说明synchronized对代码加的是轻量级锁。

偏向锁测试:

public static void testBiased02() throws InterruptedException {
        //如果休眠一段时间再加锁,那么应该加的是偏向锁
        Thread.sleep(5000);
        Dog d = new Dog();
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }

撤销对象的偏向状态方法

撤销偏向锁的三种情况,一种是调JVM参数 -XX:-UseBiasedLocking ,一种是调用对象的hashcode方法,还有一种是多个线程不同时间访问同一对象

hashcode撤销偏向

public static void testBiased03() throws InterruptedException {
        //如果休眠一段时间再加锁,那么应该加的是偏向锁
        Thread.sleep(5000);
        Dog d = new Dog();
        //因为调用哈希码之后会把线程id清掉,然后对应的字段换成哈希码,所以调用哈希码后也会禁用偏向锁
        d.hashCode();
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }

多线程不同时间访问撤销偏向

public static void testBiased04() throws InterruptedException {
        Thread.sleep(5000);
        //当其他线程使用偏向锁对象时,指的是不同时使用,偏向锁会升级为轻量级锁
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            synchronized (TestBiased01.class) {
                TestBiased01.class.notify();
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (TestBiased01.class) {
                try {
                    TestBiased01.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }, "t2");
        t2.start();
    }

批量重定向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID,当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会给这些对象加锁时重新偏向至加锁线程。
意思就是原本不同时的多线程共享临界区,锁会由偏向锁升级到轻量级锁,但这也只是前20次这样升级,超过20次后,偏向锁不会升级为轻量级锁,而是更新一个线程id,相当于还是偏向锁,只是偏向的线程id不一样了。
总结:jvm对于同一类的对象的撤销偏向操作超过40次时,再次创建对象直接不可偏向,只能加轻量级锁

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

就是在只有单线程的情况下,即使有synchronized,JIT也会将这个synchronized字节码执行时优化掉,因为加锁是有性能开销的,单线程情况下优化掉锁,代码执行效率更高。

posted @   理塘DJ  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示