深入了解synchronized(一)基本概念和原理

synchronized

共享变量带来的的问题

@Slf4j(topic = "c.testShare")
public class testShare {
    static int counter = 0; //共享变量
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter++;
                }
            }
        };
        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    counter--;
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("counter= {}",counter);
    }
}

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

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

根据java内存模型(JMM)得知,每个线程对静态共享变量的自增自减操作,需要在主存和工作内存中进行数据交换。

如果是单线程执行以上代码,顺序执行则不会发生指令交错,自然结果只能唯一。

如果是多线程执行,由于自增自减操作不是原子性的,根据cpu时间片的轮转,会发生上下文切换,造成指令交错。导致数据不能及时在主存更新,其他线程对共享变量操作时,会发生结果紊乱。

临界区 Critical Section

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

竞态条件 Race Condition

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

synchronized 解决方案

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

  • 阻塞式的解决方案:synchronized,Lock

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

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

互斥与同步

java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized语法

  • 加载对象上
    对于同步方法块,锁是 synchronized 括号里的对象。
synchronized(对象) // 线程1, 线程2(blocked)
{
		临界区
}
  • 加在方法上(实际锁的也是对象)

对于同步普通方法,锁是当前实例对象;

在这里插入代码片class Test{
public synchronized void test() {
 			
	}
}
等价于
class Test{
public void test() {
		synchronized(this) {
		临界区
		}
	}
}

对于静态同步方法,锁是当前类的 Class 对象;

class Test{
	public synchronized static void test() {
		临界区
	}
}
等价于
class Test{
public static void test() {
	synchronized(Test.class) {
		临界区
		}
	}
}

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

Monitor原理
每个Java对象都关联一个monitor对象,如果使用synchronized给对象上锁后,该对象头中MarkWord被设置为指向Monitor的指针。
在这里插入图片描述

  • 刚开始Monitor的owner为空。
  • 当Thread-2执行同步代码块时就会将Monitor中的owner指向Thread-2,Monitor中只有一个owner。
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

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

由此知synchronzied的原理

每个 Java 对象都有一个关联的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

同步代码块使用 monitorenter 和 monitorexit 这两个字节码指令获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。
在这里插入图片描述

synchronized的可重入性

@Slf4j(topic = "c.testKQR")
public class testKQR {
    static final  Object lock= new Object();
    public static void main(String[] args) {
       method1();
    }
    public  static void method1(){
        synchronized (lock){
            method2();
        }
    }
    public static void method2(){
        synchronized (lock){
           log.debug("synchronized支持可重入....");
        }
    }
}
  • 可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。
  • 根据上述代码,在同步代码中再次调用通过代码块,如果不支持锁重入,就会导致在method1中调用method2时锁不会释放,把自己阻塞,造成死锁。

对于synchronizd来说,可重入是显而易见的。它是同时通过在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经持有锁,就把锁的计数器加1.执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

synchronized的不公平性

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值