java并发关键字: synchronized详解:可重入、自旋、自适应自旋、锁的升级 您所在的位置:网站首页 synchronized自旋锁自旋次数 java并发关键字: synchronized详解:可重入、自旋、自适应自旋、锁的升级

java并发关键字: synchronized详解:可重入、自旋、自适应自旋、锁的升级

2023-09-24 08:14| 来源: 网络整理| 查看: 265

文章目录 一、使用synchronized1. 对象锁1.1 代码块1.2. 方法锁 2. 类锁2.1. synchronize修饰静态方法2.2. synchronize修饰Class对象 二、synchronized原理1. 加锁/释放锁的原理2. Synchronized 可重入例子3. 可见性 三、synchronized的优化1. 自旋锁 与自适应自旋锁2. 自旋锁实现的原理3. 自旋次数4. 自适应自旋锁 四、Synchronied锁的类型1. 锁消除-无锁2. 锁粗化3. 轻量级锁4. 偏向锁小结 五、Synchronized与Lock1. synchronized的缺陷2. Lock解决相应问题 六、使用Synchronized有哪些要注意的?

一、使用synchronized 1. 对象锁

对象锁包括 方法锁(默认锁对象为this) 和 同步代码块锁。

1.1 代码块

两个线程争夺的锁资源都是this,线程1必须要等到线程0释放了该锁后,才能执行。

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instance = new SynchronizedObjectLock(); @Override public void run() { synchronized (this) { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); } } 我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束 1.2. 方法锁

对于普通方法,锁对象默认是this。

下面代码两个线程获取的锁是不同的实例,所以线程之间不存在竞争关系。

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在普通方法上,默认的锁就是this,当前实例 public synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { // t1和t2对应的this是两个不同的实例,所以代码不会串行 Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } 我是线程Thread-0 我是线程Thread-1 Thread-1结束 Thread-0结束

 

2. 类锁

类锁指的是:synchronize修饰静态的方法或Class对象

2.1. synchronize修饰静态方法

因为锁是类,所以虽然创建了两个实例,但两个线程要竞争的锁都是Class对象。

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } public static synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } 我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束

 

2.2. synchronize修饰Class对象 public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { // 所有线程需要的锁都是同一把 synchronized(SynchronizedObjectLock.class){ System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } 我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束

   

二、synchronized原理 1. 加锁/释放锁的原理

我们先反编译下面的类,看到有monitorenter、monitorexit指令。

/** * javac SynchronizedDemo2.java * javap -verbose SynchronizedDemo2.class * * public void method1(); * descriptor: ()V * flags: ACC_PUBLIC * Code: * stack=2, locals=3, args_size=1 * 0: aload_0 * 1: getfield #3 // Field object:Ljava/lang/Object; * 4: dup * 5: astore_1 * 6: monitorenter * 7: aload_1 * 8: monitorexit * 9: goto 17 * 12: astore_2 * 13: aload_1 * 14: monitorexit * 15: aload_2 * 16: athrow * 17: invokestatic #4 // Method method2:()V * 20: return * Exception table: * from to target type * 7 9 12 any * 12 15 12 any */ public class SynchronizedDemo2 { Object object = new Object(); public void method1() { synchronized (object) { } } }

monitorenter、monitorexit 代表锁计数的操作,monitorenter锁计数器 +1 ,monitorexit锁计数器 -1 。

一个monitor在同一时间只能被一个线程获得,当一个线程获取到monitor后,就涉及到锁计数器的操作了,具体的:

对于monitorenter:

线程还没获取到monitor时,monitor计数器为0,当获取到锁时,计数器 +1,此时计数器不为零,其他线程也就获取不到monitor了。当拿到monitor的线程,又重新进入了这把锁,计数器 +1,随着重入的次数,会一直累加。

对于monitorexit:

执行完锁限定的代码后,计数器就会 -1 ,当减到计数器 =0 时, 代表线程释放了锁。其他线程就可以竞争获取锁了。

再看一下各个线程在竞争锁时,对象、对象所对应的监视器、同步队列以及线程状态的情况: 在这里插入图片描述 可以看到线程如果想对object进行访问,首先需要获取监视器,如果获取失败就进入同步队列,并为同步状态(blocked),当监视器被释放之后,在同步队列中的线程就有机会重新获取该监视器。  

2. Synchronized 可重入例子

先描述下线程的可重入性:对于获取锁的线程会获取monitor,当再次进入到(这把锁修饰的)其他代码时,monitor的计数器就会 + 1,而不是需要重新获取锁。

public class SynchronizedDemo { public static void main(String[] args) { SynchronizedDemo demo = new SynchronizedDemo(); demo.method1(); } private synchronized void method1() { System.out.println(Thread.currentThread().getId() + ": method1()"); method2(); } private synchronized void method2() { System.out.println(Thread.currentThread().getId()+ ": method2()"); method3(); } private synchronized void method3() { System.out.println(Thread.currentThread().getId()+ ": method3()"); } }

执行monitorenter获取锁(monitor计数器=0,可获取锁)

执行method1()方法,monitor计数器+1 -> 1 (获取到锁)执行method2()方法,monitor计数器+1 -> 2执行method3()方法,monitor计数器+1 -> 3

执行monitorexit命令

method3()方法执行完,monitor计数器-1 -> 2method2()方法执行完,monitor计数器-1 -> 1method2()方法执行完,monitor计数器-1 -> 0 (释放了锁) (monitor计数器=0,锁被释放了)   3. 可见性

happens-before规则中有:对同一个监视器的解锁,happens-before于对该监视器的加锁。

说的直白一点:解锁前线程A所做的修改,解锁后获取锁的线程B能够看到。

   

三、synchronized的优化 1. 自旋锁 与自适应自旋锁

没有加入锁优化时,synchronized很重:多线程竞争锁时,一个线程获取锁时,会堵塞其他所有线程,这会引起性能问题。   具体的:阻塞或唤醒线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。   我们可以让没有获取的锁自旋,不放弃CPU的执行时间,等待锁的释放。

2. 自旋锁实现的原理

自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

3. 自旋次数

在JDK 1.6后默认开启自旋锁,如果锁占用的时间很短,那自旋锁的性能会很高。相反,当锁被占用的时间较长时,自旋锁会适得其反,因为线程在自旋时会始终占有CPU资源,所以自旋的时间要有一定的限制,默认是自旋10次,如果超过10次就应该挂起线程了。

4. 自适应自旋锁

在JDK 1.6中引入了自适应自旋锁来继续提高并发性能。 具体的:假设通过自旋等待刚刚成功获得过锁,那JVM会认为该线程通过自旋的概率很大,此时线程会自动增加自旋时间。相反,如果通过自旋很少能获取锁,那之后可能会直接忽略掉此线程的自旋过程,直接挂起线程。

   

四、Synchronied锁的类型

在JDK 1.6 Synchronied锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着线程的竞争情况而升级,同时为了锁的获取和释放的效率,锁只允许升级而不允许升级。

1. 锁消除-无锁

JVM会判断一段程序的同步明显不会逃逸出去被其他线程访问到,那就会把他们当作栈上的数据来看待,认为是线程私有的,此时就会进行锁消除。

逃逸分析:逃逸分析的基本行为是分析对象动态作用域。比如:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。   同步:同步指的是线程间的协作,按照一定的规则线程依次运行。例如线程互斥:当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。

看一个例子: String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String连接做自动优化。

/** * public java.lang.String test03(java.lang.String, java.lang.String, java.lang.String); * descriptor: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; * flags: ACC_PUBLIC * Code: * stack=2, locals=5, args_size=4 * 0: new #6 // class java/lang/StringBuilder * 3: dup * 4: invokespecial #7 // Method java/lang/StringBuilder."":()V * 7: aload_1 * 8: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; * 11: aload_2 * 12: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; * 15: aload_3 * 16: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; * 19: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; * 22: astore 4 * 24: aload 4 * 26: areturn * LineNumberTable: * line 44: 0 * line 45: 24 */ public static String test03(String s1, String s2, String s3) { String s = s1 + s2 + s3; return s; }

在JDK 1.5之前会使用StringBuffer (线程安全) 对象的连续append()操作。 在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。

 

2. 锁粗化

如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作。

我们看JVM是如何解决此问题的:

public static String test04(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); } //StringBuffer append源码 public synchronized StringBuffer append(StringBuffer sb) { toStringCache = null; super.append(sb); return this; }

上述连续append()操作中就属于连续加锁解锁的情况,JVM会将加锁同步的范围扩展(粗化)到整个一连串的append()操作,即这些append只需要加锁一次即可。

 

3. 轻量级锁

要理解轻量级锁,先要了解HotSpot虚拟机中对象头的内存布局。

在对象头中(Object Header)存在两部分。 第一部分:用于存储对象自身的运行时数据,HashCode、GC Age、锁标记位、是否为偏向锁等。官方称之为Mark Word。 第二部分:存储的是Class对象的类型指针(Klass Point)。

在线程执行同步块之前,JVM会先在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于拷贝存储锁对象目前的Mark Word(称为:Displaced Mark Word)。

线程加锁过程:

假设当前对象没有被锁定(锁标志位为01),线程A要执行(此对象锁锁定的代码)时,首先会在当前线程栈帧中创建Lock Record空间,然后CAS操作将Mark Word拷贝到Lock Record中,并将Mark Word的指向更新为Lock Record指针。   如果更新成功了,那这个线程就有了该对象的锁,并将Mark Word的锁标记为更新为00,表示此对象处于轻量级锁定状态。   如果没有更新成功,则判断此对象Mark Word的指向,如果指向当前线程则说明已经获取到了锁,可以直接调用,如果没有指向当前线程,则说明该锁被其他线程占有。   当有两个线程竞争同一个锁,则此轻量锁就不再有效,直接膨胀为重量级锁(锁标志位为10,Mark Word中存储指向重量级锁的指针)。

线程解锁过程: 轻量级锁解锁时,会使用CAS将 Displaced Mard Word 替换回 Mark Word 中(相当于复原)。如果替换成功,则说明期间没有其他线程访问同步代码块。

如果替换(CAS)失败,表示当前线程在执行同步代码块期间,有其他线程也在访问,当前锁资源是存在竞争的,那么锁将会膨胀成重量级锁。

 

4. 偏向锁

当一个线程访问同步块获取到锁时,将对象头中的ThreadID改成自己的ID,以后该线程进入同步块时,只需要对比ID,而不需要CAS操作。

偏向锁升级为轻量级锁

因为偏向锁不会主动释放,所以当第二个线程访问这个对象时,可以看到对象的偏向状态。这时表明已经存在竞争,检查原来持有该对象锁的线程是否存活。   如果挂了,则将对象设置为无锁状态,然后重新偏向新的线程;   如果依然存活,继续判断该对象的是否需要持有偏向锁,如果需要锁则升级为轻量级锁。如果不在使用了,则将对象恢复成无锁状态,然后重新偏向。

 

小结

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

锁特点使用场景偏向锁加锁和解锁不需要CAS操作,没有额外的性能消耗。如果线程存在竞争会带来额外的锁撤销的消耗适用于只有一个线程访问同步块的场景轻量级锁通过自旋使竞争的线程不会堵塞,提高了响应速度。但如果线程始终得不到锁,自旋会带来CPU的空转。追求响应时间,同步块代码少的情况重量级锁线程挂起不使用自旋,不消耗CPU。但线程堵塞,响应时间缓慢,在多线程下,频繁的获取锁,消耗性能追求吞吐量,同步块执行速度较长

 

五、Synchronized与Lock 1. synchronized的缺陷 缺陷解释效率低只有执行完代码块或异常时才能释放锁;不能中断一个正在使用锁的线程;获取锁时不能设定超时。不够灵活加锁方式单一、每个锁只有一个单一的条件(某个对象或类)无法知道是否成功获得锁相对而言,Lock可以拿到状态,如果成功获取锁,…,如果获取失败,…

 

2. Lock解决相应问题

Lock类的4个方法:

lock(): 加锁 unlock(): 解锁 tryLock(): 尝试获取锁,返回一个boolean值 tryLock(long,TimeUtil): 尝试获取锁,可以设置超时 六、使用Synchronized有哪些要注意的? 锁对象不能为空,因为锁的信息都保存在对象头里作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错避免死锁在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错。

     参考: https://pdai.tech/md/java/thread/java-thread-x-key-synchronized.html



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有