JUC并发编程——对于synchronized关键字的理解

现象🔍:

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,最后输出的 counter一定为0 吗?

@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 为静态变量),实际会产生的字节码指令:

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

而对应 i-- 也是类似:

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

在多线程情况下,这八行代码可能会交错执行。

解决方法

使用 synchronized 加锁,保证了代码块内的原子性

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虚拟机的指令集中 有monitorentermonitorexit两条指令来支持synchronized关键字的语义。

根据《Java虚拟机规范》的要求,
在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

  • 被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。

    例如在切换上下文进程时,可能带有锁的A线程并没有释放,但切换到了没有锁的B线程,不能进入synchronized块内,当轮到A线程时,A线程会带有那把锁的钥匙,再次进入synchronized块内,直到释放锁。

  • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。
    这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。

如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转换需要耗费很多的处理器时间。状态转换的时间甚至比执行用户代码的时间还长。

方法上的 synchronized

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

Synchronized 的底层原理

1)对象头

想要理解 synchronized 的工作流程,需要对HotSpot虚拟机对象的内存布局(尤其是对象头部分)有所了解。HotSpot 虚拟机的对象头(Object Header)分为两部分,第一部分用于存储对象自身的运行时数据(Mark Word)和用于存储指向方法区对象类型数据的指针(类指针)。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态
复用自己的存储空间。在32位, 64位操作系统中的空间不一样,这里以32位的操作系统为例。

在这里插入图片描述

2)Monitor 原理

Monitor 被翻译为监视器管程,Monitor 是操作系统管理的。synchronized 底层会被 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 中等待的线程来竞争锁,竞争的时是非公平的

3)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开始)
			 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值