如何查看网站抓取频率,wordpress被封,用易语言可以做网站吗,网站建设教材下载介绍synchronized synchronized 是Java编程语言中的一个关键字#xff0c;用于实现线程间的同步。在多线程环境下#xff0c;它确保了共享资源在同一时刻只能被一个线程访问或修改#xff0c;从而避免了因多个线程并发操作同一数据而导致的数据不一致和竞态条件。 synchroni…介绍synchronized synchronized 是Java编程语言中的一个关键字用于实现线程间的同步。在多线程环境下它确保了共享资源在同一时刻只能被一个线程访问或修改从而避免了因多个线程并发操作同一数据而导致的数据不一致和竞态条件。 synchronized可以用来修饰实例方法、静态方法、代码块。 当synchronized修饰实例方法时它获取的是当前对象实例即 this的内置锁。当一个线程调用这个方法时其他线程必须等待该线程执行完此方法后才能再次获得对该类实例的访问权限并执行这个同步方法。
/*** synchronized修饰实例代码*/
public class SynchronizedDemo { public synchronized void method(){// 业务逻辑}
} 当synchronized修饰静态方法时它获取的是当前Class的锁静态成员属于当前类的不属于实例对象后面有案例分析对在同一类中对静态方法加锁和实例加锁的是否互斥。
/*** synchronized修饰实例代码*/
public class SynchronizedDemo { public synchronized static void method(){// 业务逻辑}
} 当synchronized修饰代码块时通过指定一个特定的对象或者Class作为锁。在同步代码块中当线程进入时会获取到指定对象或者Class的锁退出时释放锁。这种方式更灵活因为可以决定锁住哪个对象而不是默认锁定整个方法的调用者。需要注意的是不要使用String作为锁的对象因为jvm为了避免字符串重复创建开辟了一块专门的区域存放字符串字符串常量池字符串常量池会缓存字符串对象的引用。 public class SynchronizedDemo { private final Object lock new Object();public void method(){synchronized (lock){// 业务逻辑}}
}
分析加锁对象 通过一些案例让大家对synchronized加锁时锁的是哪个对象有更深的印象。 定义一个Test对象对象中有read、write两个方法在LockObjectDemo中开启两个线程分别调用read、write方法。
// Test.javapublic void read(){System.out.println(test read----);// 睡眠一段时间更方便查看锁的对象try {Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}}public void write(){System.out.println(test write----);}//LockObjectDemo.javapublic static void main(String[] args) {Test test new Test();new Thread(() - {test.read();},a).start();new Thread(() - {test.write();},b).start();}
无锁的运行结果如下线程a与线程b各自执行调用的相关方法互不影响。 案例一a、b两个线程分别访问同一个Test对象中的不同方法(read、write方法用synchronized修饰),运行结果如下。 分析a、b两个线程分别访问同一个Test对象中的不同方法(read、write方法用synchronized修饰)锁的是当前实例对象Test实例a线程先获得锁b线程需要等待a线程释放锁才能继续往下执行。 一个对象里面如果有多个synchronized方法某一时刻内只要有一个线程去调用其中的一个synchronized方法其他的线程都只能等待换句话说某一时刻内只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this被锁定后其他线程都不能进入到当前对象的其他synchronized方法中 案例二同案例一条件不变在Test对象中新增一个普通方法在LockObjectDemo新启动一个线程调用普通方法运行结果如下。 分析线程c中调用新加的普通方法不受锁的影响线程a和线程b继续竞争锁。 案例三 分别声明t1、t2Test对象a线程调用t1对象的read方法、b线程调用t2对象的write方法(read、write方法用synchronized修饰),运行结果如下。 分析a线程获取的是t1对象的锁b线程获取的是t2对象的锁两者获得的锁不是同一把所以调用方法互不影响。 Test t1 new Test();Test t2 new Test();new Thread(() - {t1.read();},a).start();new Thread(() - {t2.write();},b).start(); 案例四a、b两个线程分别访问同一个Test对象中的不同方法(read、write方法用static synchronized修饰)。 案例五分别声明t1、t2Test对象a线程调用t1对象的read方法、b线程调用t2对象的write方法(read、write方法用static synchronized修饰), 案例四与案例五运行结果相同运行结果如下。 分析为什么案例四与案例五运行结果是一样的案例一与案例三运行结果不同。我们知道statci成员归类所有static成员在类加载的时候就会被分配内存通过类名就能访问这里通过实例对象访问静态成员的方式只是为了更方便的查看线程获取的是当前实例还是当前类的锁。案例四中的a、b线程虽然调用的是同一个实例对象的不同static方法实际这两个线程获取的是Test.Class的锁而不是实例对象的锁。案例五a、b线程通过不同实例对象调用相关方法竞争的还是Test.Class这个类锁。 案例六a、b两个线程分别访问同一个Test对象中的不同方法(read方法用synchronized修饰、write方法用static synchronized修饰)。运行结果如下。 分析a线程获取的是实例对象的锁b线程获取的是当前类的锁锁的对象不同两个线程之间不会发生竞争。 synchronized深入探究 字节码分析 先从字节码层面分析synchronized修饰的代码块、实例方法、静态方法。 先编写一个synchronized修饰的代码块程序实示例再通过javap -c class名字 命令反编译。通过反编译后的代码可以看到synchronized修饰的代码块是通过monitorenter和monitorexit指令来保证锁的获取和释放有疑问的是为什么出现了两个monitorexit指令第一个monitorexit指令保证代码块正常执行后释放锁如果代码块中出现异常导致代码执行中断第二个monitorexit指令会将锁释放。
//java程序示例
private Object object new Object();public void sync(){synchronized (this.object){// 业务逻辑System.out.println(hello);}
}//javap -c clss名称 反编译后的代码
public void sync();Code:0: aload_01: getfield #3 // Field object:Ljava/lang/Object;4: dup5: astore_16: monitorenter7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;10: ldc #5 // String hello12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V15: aload_116: monitorexit17: goto 2520: astore_221: aload_122: monitorexit23: aload_224: athrow25: returnException table:from to target type7 17 20 any20 23 20 any反编译一下synchronized修饰的普通实例方法没有monitorenter和monitorexit指令了取而代之的是方法flags里的ACC_SYNCHRONIZED标识通过设置这个标识jvm会自动识别给这个方法加锁等方法执行完后无论是否异常都会释放锁。 synchronized修饰的静态方法与普通实例同步方法相比只是多了一个ACC_STATIC标识以此来判断是获取类锁还是实例对象的锁。
//普通实例方法
public synchronized void sync1(){//业务逻辑System.out.println(hello);
}//javap -v class名称 反编译后的synchronized普通实例方法
public synchronized void sync1();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack2, locals1, args_size10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #5 // String hello5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 23: 0line 24: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this Lcom/juc/chapter12/SynDemo2;//静态方法
public synchronized static void sync1(){//业务逻辑System.out.println(hello);
}//javap -v class名称 反编译后的synchronized静态方法
public static synchronized void sync1();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZEDCode:stack2, locals0, args_size00: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #5 // String hello5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 16: 0line 17: 8管程 引用《深入理解Java虚拟机JVM高级特性与最佳实践第3版》中的一段话 Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步这两种同步结构都是使用管程Monitor更常见的是直接将它称为“锁”来实现的。 管程Monitor也称为监视器是一种程序结构结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点最多只有一个线程在执行管程的某个子程序。管程提供了一种机制管程可以看做一个软件模块它是将共享的变量和又于这些共享变量的操作封装起来形成一个具有一定接口的功能模块进程可以调用管程来实现进程级别的并发控制。 在jvm层次会为每个java对象创建一个相关联的管程Monitor多个线程竞争对象锁实际上竞争的是对象关联管程的操作权限。执行monitorenter指令尝试去获取管程Monitor的操作权限如果获取失败表示已有其他的线程持有了此对象的锁需要等待monitorexit指令释放锁再次尝试去获取管程Monitor的操作权限。 ObjectMonitor 在HotSpot虚拟机中Monitor是通过ObjectMonitor实现的ObjectMonitor定义的基本信息如下 // jvm源码路径- src/share/vm/runtime/objectMonitor.hppObjectMonitor() {_header NULL;_count 0; //用来记录该线程获取锁的次数_waiters 0,_recursions 0; //线程的重入次数_object NULL; //存储该monitor的对象_owner NULL; //指向持有该monitor的线程_WaitSet NULL; // 将处于等待状态的线程加入到该队列中_WaitSetLock 0 ;_Responsible NULL ;_succ NULL ;_cxq NULL ; //多线程竞争锁时的队列FreeNext NULL ;_EntryList NULL ; //存放处于等待锁block状态的线程队列_SpinFreq 0 ;_SpinClock 0 ;OwnerIsThread 0 ;_previous_owner_tid 0;}// jvm源码路径- src/share/vm/runtime/objectMonitor.cpp// enter、exit、wait、notify方法这里不做过多解析感兴趣的朋友可以参考// https://blog.csdn.net/lhm964517471/article/details/131710893 锁升级 在Java早期版本中synchronized属于重量级锁效率低下因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock(系统瓦斥量)来实现的挂起线程和恢复线程都需要转入内核态去完成阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成这种状态切换需要耗费处理器时间如果同步代码块中内容过于简单这种切换的时间可能比用户代码执行的时间还长”时间成本相对较高这也是为什么早期的synchronized效率低的原因Java 6之后为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁。 在Java 6中锁从低到高一共有四种状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略目的是为了提高获得锁和释放锁的效率。 对象内存布局 锁在竞争的情况下具体怎么升级的首先从了解对象的内存布局开始。在HotSpot虚拟机里对象在堆内存中的存储布局分为三个部分对象头、实例数据、对象填充。 对象头包含Mark Word两部分。Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。在64位虚拟中Mark Word占用8个字节64bit类型指针也占用8个字节。 Mark Word存储内容如下
存储内容标志位状态对象哈希码、对象分代年龄01未锁定指向锁记录的指针00轻量级锁定指向重量级锁的指针10膨胀重量级锁定空不需要记录信息11GC标记偏向线程ID、偏向时间戳、对象分代年龄01可偏向 类型指针即对象指向它的类型元数据的指针Java虚拟机通过这个指针来确定该对象是哪个类的实例。 实例数据存放类的属性数据信息包括父类的属性信息。 对齐填充 Java虚拟机要求对象起始地址必须是8字节的整数倍。不足8字节整数倍的对象会被填充到8字节整数倍。可以通过导入jol.jar包查看对象的对齐填充示例如下 public static void main(String[] args) {Object o new Object();// 打印一个对象的布局System.out.println(ClassLayout.parseInstance(o).toPrintable());} 无锁 对象处于无锁状态时64位jvm虚拟机中Mark Word存储的信息如下 偏向锁 大多数情况下锁不存在多线程竞争并且总是由同一线程多次获得为了让线程获得锁的代价更低而引入了偏向锁。 64位jvm虚拟机中处于偏向锁状态下的Mark Word存储的信息如下 当一个线程访问同步块并获取锁时会在对象头和栈帧中的锁记录里存储锁偏向的线程ID以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功表示线程已经获得了锁。如果测试失败则需要再测试一下Mark Word中偏向锁的标识是否设置成1表示当前是偏向锁如果没有设置则使用CAS竞争锁如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程。 偏向锁等到其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放锁。偏向锁的撤销需要等待全局安全点在这个时间点上没有正在执行的字节码。它会首先暂停拥有偏向锁的线程然后检查持有偏向锁的线程是否活着如果线程不处于活动状态则将对象头设置成无锁状态如果线程仍然活着拥有偏向锁的栈会被执行遍历偏向对象的锁记录栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程要么恢复到无锁或者标记对象不适合作为偏向锁最后唤醒暂停的线程。 注意2020年9月发布的JDK15中有一项新声明JEP 374: Disable and Deprecate Biased Locking 在默认情况下禁用偏向锁定并弃用所有相关命令行选项。 轻量级锁 当偏向锁功能关闭或者多线程竞争偏向锁偏向锁会升级为轻量级锁。 64位jvm虚拟机中处于轻量级锁状态下的Mark Word存储的信息如下 轻量级锁加锁线程在执行同步块之前JVM会先在当前线程的栈桢中创建用于存储锁记录的空间并将对象头中的Mark Word复制到锁记录中官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功当前线程获得锁如果失败表示其他线程竞争锁当前线程便尝试使用自旋来获取锁。 轻量级锁解锁会使用原子的CAS操作将Displaced Mark Word替换回到对象头如果成 功则表示没有竞争发生。如果失败表示当前锁存在竞争锁就会膨胀成重量级锁。 重量级锁 java中synchronized中的重量级锁是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitorenter指令在结束位置插入monitorexit指令当线程执行到monitorenter指令时会尝试获取对象所对应的Monitor所有权如果获取到了即获取到了锁会在Monitor的_owner中存放当前线程的id这样它将处于锁定状态除非退出同步块否则其他线程无法获取到这个Monitor。 64位jvm虚拟机中处于重量级锁状态下的Mark Word存储的信息如下 synchronized、对象、Moniter的联系如下 可重入锁 可重入锁又名递归锁是指同一个线程在外层方法获取锁的时候在进入该线程的内部方法会自动获取锁前提锁对象是同一个对象不会因为之前已经获取过锁还没释放而阻塞。 public synchronized void a(){b();System.out.println(current method a);}public synchronized void b(){c();System.out.println(current method b);}public synchronized void c(){System.out.println(current method c);} synchronized和reentrantLock都是可重入锁可重入锁在一定程度上可避免死锁。 synchronized可重入锁的实现机制每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针当执行monitorenter时如果目标锁对象的计数器为0那么说明它没有被其他线程持有java虚拟机会将该锁对象的持有线程设置为当前线程并且将其计数器1。在目标锁对象的计数器不为0的情况下如果锁对象的持有对象时当前线程那么java虚拟机可以将其计数器1否则需要等待直至持有线程释放该锁 当指向monitorexit时java虚拟机则需将锁对象的计数器-1计数器为0代表锁被释放。 这里的锁对象指的是monitor每一个对象都有一个monitor在Java虚拟机(HotSpot)中monitor是由ObjectMonitor实现的。从ObjectMonitor源码中我们可以看到锁计数器、指向持有该锁的线程的指针存放的位置。 锁消除 消除是指虚拟机即时编译器在运行时对一些代码要求同步但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持如果判断到一段代码中在堆上的所有数据都不会逃逸出去被其他线程访问到那就可以把它们当作栈上数据对待认为它们是线程私有的同步加锁自然就无须再进行。 public void sync(){// object的引用不会逃逸到方法外其他线程无法访问到object// 这段代码再解释执行时会加锁在经过服务端编译器的即时编译之后这个同步措施会被忽略// 实际开发中一般不会这么写Object lock new Object();synchronized (lock){System.out.println(hello word);}} 锁粗化 如果一系列的连续操作都对同一个对象反复加锁和解锁甚至加锁操作是出现在循环体之中的那即使没有线程竞争频繁地进行互斥同步操作也会导致不必要的性能损耗。虚拟机探测到有这样一串零碎的操作都对同一个对象加锁将会把加锁同步的范围扩展粗化到整个操作序列的外部。 static Object lock new Object();public void sync(){synchronized (lock){System.out.println(a);}synchronized (lock){System.out.println(b);}synchronized (lock){System.out.println(c);}}// JIT编译器编译后 -- 锁粗化public void sync(){synchronized (lock){System.out.println(a);System.out.println(b);System.out.println(c);}}
总结
1.互斥同步多个线程访问共享数据时在同一时刻只能有一个线程访问保证数据正确性。
2.synchronized作用对象是非静态的获得锁是对象锁如果是静态的获得锁则是类锁。
3.synchronized锁是可重入的同一个线程在外部获取锁以后在内部也能自动获取锁前提是同一个对象。
4.在Java早期版本中synchronized属于重量级锁效率低下Java 6之后为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁。
5.JDK15后默认禁用偏向锁定。
参考
【1】深入理解Java虚拟机JVM高级特性与最佳实践第3版
【2】Java并发编程的艺术
【3】Java——聊聊JUC中的锁synchronized Lock ReentrantLock_51CTO博客_java锁synchronized原理【4】Java多线程objectMonitor源码解析4-CSDN博客
【5】b站 - 尚硅谷JUC并发编程