并发学习记录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字节码执行时优化掉,因为加锁是有性能开销的,单线程情况下优化掉锁,代码执行效率更高。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具