目录
前言
一、synchronized的工作原理
二、使用synchronized代码块的场景
三、编写synchronized代码块的最佳实践
四、何时使用 synchronized 代码块?
同步:
不同步:
五、Demo讲解
1.使用synchronized代码块减小锁的粒度,提高性能
2.synchronized可以使用任意的object进行加锁
3.不要使用String的常量加锁,会出现死循环问题
4.锁对象的改变问题
5.死锁问题
前言
在多线程编程中,确保共享资源的安全访问是一个关键问题。Java 提供了多种机制来实现线程同步,其中 synchronized
关键字是最常用的一种。在本文中,我们将深入探讨 synchronized
代码块的使用和原理,并通过示例展示其应用。
一、synchronized的工作原理
synchronized
关键字可以应用于方法或代码块,以确保同一时间只有一个线程可以执行被synchronized
修饰的代码。其工作原理主要基于Java对象头中的Monitor(监视器)。每个Java对象都有一个与之关联的Monitor,线程在访问synchronized
代码块时,需要首先获得该对象的Monitor的所有权。
- synchronized方法:当一个线程进入一个对象的
synchronized(this)
方法时,它自动获取该对象的Monitor的所有权,并在方法返回或抛出异常时释放Monitor。 - synchronized代码块:
synchronized
代码块允许我们更精细地控制哪些代码需要被同步。线程在访问synchronized(object)
代码块时,需要获得指定对象object
的Monitor的所有权。
二、使用synchronized代码块的场景
- 保护共享资源:当多个线程需要访问和修改同一份数据时,可以使用
synchronized
代码块来确保同一时间只有一个线程能够访问这些数据。 - 避免死锁:与
synchronized
方法相比,synchronized
代码块提供了更细粒度的同步控制,有助于减少死锁的风险。通过只同步必要的代码段,可以减少线程之间的竞争和等待时间。 - 提高性能:在某些情况下,使用
synchronized
代码块可以避免不必要的同步开销。例如,当一个方法中的大部分代码都不需要同步时,可以将需要同步的代码段封装在synchronized
代码块中。
三、编写synchronized代码块的最佳实践
- 尽量减小同步范围:只将需要同步的代码段放在
synchronized
代码块中,避免不必要的同步开销。 - 避免在同步块中调用可能阻塞的方法:在同步块中调用可能阻塞的方法(如IO操作、等待用户输入等)会导致其他等待Monitor的线程也被阻塞,从而降低系统的并发性能。
- 注意锁的粒度:过细的锁粒度可能导致线程之间的竞争加剧,而过粗的锁粒度则可能导致不必要的同步开销。因此,在选择锁的粒度时需要权衡这两个因素。
- 避免嵌套锁:尽量避免在一个已经持有某个对象Monitor的线程中再次请求该对象或其他对象的Monitor。这可能导致死锁或其他并发问题。
- 考虑使用ReentrantLock等高级并发工具:虽然
synchronized
关键字简单易用,但在某些复杂场景下可能需要更灵活的同步控制。此时可以考虑使用Java并发包中的ReentrantLock、Semaphore等高级并发工具。
四、何时使用 synchronized 代码块?
同步:
- 访问共享资源:当多个线程需要访问同一个对象或变量时,应该使用同步。
- 修改共享状态:当多个线程修改同一个对象的状态时,应该使用同步。
- 执行原子操作:当需要保证某些操作的原子性(即操作不可分割)时,应该使用同步。
不同步:
- 只读操作:如果多个线程只是读取共享资源,而不修改它,不需要同步。
- 局部变量:局部变量是线程私有的,不需要同步。
- 无共享资源:如果多个线程操作的资源相互独立,不需要同步。
五、Demo讲解
1.使用synchronized代码块减小锁的粒度,提高性能
使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,也就是通常所说的减小锁的粒度。
package com.ctb.sync6;/*** 使用synchronized代码块减小锁的粒度,提高性能* * @author biao** 2024年*/
public class Optimize {public void doLongTimeTask(){try {System.out.println("当前线程开始:" + Thread.currentThread().getName() + ", 正在执行一个较长时间的业务操作,其内容不需要同步");Thread.sleep(2000);synchronized(this){System.out.println("当前线程:" + Thread.currentThread().getName() + ", 执行同步代码块,对其同步变量进行操作");Thread.sleep(1000);}System.out.println("当前线程结束:" + Thread.currentThread().getName() +", 执行完毕");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {final Optimize otz = new Optimize();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {otz.doLongTimeTask();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {otz.doLongTimeTask();}},"t2");t1.start();t2.start();}}
结果:
注:使用 synchronized 代码块减小锁的粒度,以提高多线程程序的性能。在
doLongTimeTask()
方法中,通过将长时间业务操作与需要同步的操作分别放置在不同的代码块中,两个线程可以更有效地并发执行需要同步的操作,可以减小锁的粒度,使得只有在必要的部分才会被同步,从而提高了并发执行的效率。这样做的好处是在确保线程安全的同时,尽量减少同步的范围,避免不必要的阻塞,从而提升程序的性能。也是多线程编程中常用的优化手段之一
2.synchronized可以使用任意的object进行加锁
package com.ctb.sync6;/*** 使用synchronized代码块加锁,比较灵活* * @author biao** 2024年*/
public class ObjectLock {public void method1(){synchronized (this) { //对象锁try {System.out.println("do method1..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}public void method2(){ //类锁synchronized (ObjectLock.class) {try {System.out.println("do method2..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}private Object lock = new Object();public void method3(){ //任何对象锁synchronized (lock) {try {System.out.println("do method3..");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final ObjectLock objLock = new ObjectLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method1();}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method2();}});Thread t3 = new Thread(new Runnable() {@Overridepublic void run() {objLock.method3();}});t1.start();t2.start();t3.start();}}
结果:
注:在多线程环境下使用 synchronized 代码块来实现不同类型锁的加锁操作,通过展示对象锁、类锁和任意对象锁的使用方式,说明了不同加锁方式在多线程并发环境中的作用,用法比较灵活。
3.不要使用String的常量加锁,会出现死循环问题
package com.ctb.sync6;
/*** synchronized代码块对字符串的锁,注意String常量池的缓存功能* * @author biao** 2024年*/
public class StringLock {public void method() {//new String("字符串常量")synchronized ("字符串常量") {try {while(true){System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");Thread.sleep(1000); System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");}} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final StringLock stringLock = new StringLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {stringLock.method();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {stringLock.method();}},"t2");t1.start();t2.start();}
}
结果:
注:在
StringLock
类中,定义了一个method()
方法,该方法使用"字符串常量"
作为锁对象来实现同步操作。在方法中,通过 synchronized("字符串常量") 来对代码块进行加锁,确保多个线程在执行该代码块时是互斥的。在
main
方法中,创建了两个线程 t1 和 t2 分别执行method()
方法。由于两个线程共享同一个字符串常量作为锁对象,因此它们在执行method()
方法时会相互竞争这个锁。
public void method() {//new String("字符串常量")synchronized (new String("字符串常量")) {try {while(true){System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");Thread.sleep(1000); System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");}} catch (InterruptedException e) {e.printStackTrace();}}}
结果:
注:“字符串常量”它是只有一个引用,尽量不要拿字符串常量这种方式去加锁,我们可以使用new String("字符串常量"),注意即可
4.锁对象的改变问题
当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同,如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。
package com.ctb.sync6;
/*** 锁对象的改变问题* * @author biao** 2024年*/
public class ChangeLock {private String lock = "lock";private void method(){synchronized (lock) {try {System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");lock = "change lock";Thread.sleep(2000);System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final ChangeLock changeLock = new ChangeLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {changeLock.method();}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {changeLock.method();}},"t2");t1.start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}
结果:
注:当我们使用字符串常量作为一把锁的时候,一定要注意在synchronized代码块里尽量不要去修改该锁对象的内容,第一个线程拿到的锁lock的值是lock,第二个线程拿到的是change lock去获得锁
private void method(){synchronized (lock) {try {System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");//lock = "change lock";Thread.sleep(2000);System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");} catch (InterruptedException e) {e.printStackTrace();}}}
结果:
注://lock = "change lock";当我们不去修改该锁对象的内容时,他将会依次去获取锁。
package com.ctb.sync6;
/*** 同一对象属性的修改不会影响锁的情况* * @author biao** 2024年*/
public class ModifyLock {private String name ;private int age ;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public synchronized void changeAttributte(String name, int age) {try {System.out.println("当前线程 : " + Thread.currentThread().getName() + " 开始");this.setName(name);this.setAge(age);System.out.println("当前线程 : " + Thread.currentThread().getName() + " 修改对象内容为: " + this.getName() + ", " + this.getAge());Thread.sleep(2000);System.out.println("当前线程 : " + Thread.currentThread().getName() + " 结束");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {final ModifyLock modifyLock = new ModifyLock();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {modifyLock.changeAttributte("张三", 20);}},"t1");Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {modifyLock.changeAttributte("李四", 21);}},"t2");t1.start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}
结果:
注:由于
changeAttribute
方法使用了对象锁,因此在多线程环境下,只有一个线程能够执行该方法,保证了对同一个对象的属性修改操作是互斥的。一个对象里面的属性发生改变的时候,是不影响锁的变化的,还是这个对象。使用对象锁实现了对同一对象属性的修改操作的线程安全性,确保了多线程环境下对对象属性的修改操作是同步的,避免了数据不一致的情况发生
5.死锁问题
package com.ctb.sync6;/*** 死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况* * @author biao** 2024年*/
public class DeadLock implements Runnable{private String tag;private static Object lock1 = new Object();private static Object lock2 = new Object();public void setTag(String tag){this.tag = tag;}@Overridepublic void run() {if(tag.equals("a")){synchronized (lock1) {try {System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) {System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");}}}if(tag.equals("b")){synchronized (lock2) {try {System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入了lock2执行");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1) {System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入了lock1执行");}}}}public static void main(String[] args) {DeadLock d1 = new DeadLock();d1.setTag("a");DeadLock d2 = new DeadLock();d2.setTag("b");Thread t1 = new Thread(d1, "t1");Thread t2 = new Thread(d2, "t2");t1.start();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}
结果:
注:在
run()
方法中,如果tag
的取值为 "a",则线程会先获取lock1
,然后尝试获取lock2
;如果tag
的取值为 "b",则线程会先获取lock2
,然后尝试获取lock1
。当两个线程同时运行时,它们会陷入死锁状态,因为彼此持有对方需要的锁而无法释放,导致程序无法继续执行下去,最终需要手动终止程序。
为避免死锁,应该设计程序避免多个线程竞争多个锁的情况,或者确保多个锁的获取顺序是一致的,从而避免循环等待的情况发生。