• 深入理解 Java 中的 synchronized 代码块


    目录

    前言

    一、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 代码块?

    同步:

    1. 访问共享资源:当多个线程需要访问同一个对象或变量时,应该使用同步。
    2. 修改共享状态:当多个线程修改同一个对象的状态时,应该使用同步。
    3. 执行原子操作:当需要保证某些操作的原子性(即操作不可分割)时,应该使用同步。

    不同步:

    1. 只读操作:如果多个线程只是读取共享资源,而不修改它,不需要同步。
    2. 局部变量:局部变量是线程私有的,不需要同步。
    3. 无共享资源:如果多个线程操作的资源相互独立,不需要同步。

    五、Demo讲解

    1.使用synchronized代码块减小锁的粒度,提高性能

    使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,也就是通常所说的减小锁的粒度。

    1. package com.ctb.sync6;
    2. /**
    3. * 使用synchronized代码块减小锁的粒度,提高性能
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class Optimize {
    10. public void doLongTimeTask(){
    11. try {
    12. System.out.println("当前线程开始:" + Thread.currentThread().getName() +
    13. ", 正在执行一个较长时间的业务操作,其内容不需要同步");
    14. Thread.sleep(2000);
    15. synchronized(this){
    16. System.out.println("当前线程:" + Thread.currentThread().getName() +
    17. ", 执行同步代码块,对其同步变量进行操作");
    18. Thread.sleep(1000);
    19. }
    20. System.out.println("当前线程结束:" + Thread.currentThread().getName() +
    21. ", 执行完毕");
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. }
    26. public static void main(String[] args) {
    27. final Optimize otz = new Optimize();
    28. Thread t1 = new Thread(new Runnable() {
    29. @Override
    30. public void run() {
    31. otz.doLongTimeTask();
    32. }
    33. },"t1");
    34. Thread t2 = new Thread(new Runnable() {
    35. @Override
    36. public void run() {
    37. otz.doLongTimeTask();
    38. }
    39. },"t2");
    40. t1.start();
    41. t2.start();
    42. }
    43. }

    结果: 

    :使用 synchronized 代码块减小锁的粒度,以提高多线程程序的性能。在 doLongTimeTask() 方法中,通过将长时间业务操作与需要同步的操作分别放置在不同的代码块中,两个线程可以更有效地并发执行需要同步的操作,可以减小锁的粒度,使得只有在必要的部分才会被同步,从而提高了并发执行的效率。

    这样做的好处是在确保线程安全的同时,尽量减少同步的范围,避免不必要的阻塞,从而提升程序的性能。也是多线程编程中常用的优化手段之一

    2.synchronized可以使用任意的object进行加锁

    1. package com.ctb.sync6;
    2. /**
    3. * 使用synchronized代码块加锁,比较灵活
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class ObjectLock {
    10. public void method1(){
    11. synchronized (this) { //对象锁
    12. try {
    13. System.out.println("do method1..");
    14. Thread.sleep(2000);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. public void method2(){ //类锁
    21. synchronized (ObjectLock.class) {
    22. try {
    23. System.out.println("do method2..");
    24. Thread.sleep(2000);
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. }
    30. private Object lock = new Object();
    31. public void method3(){ //任何对象锁
    32. synchronized (lock) {
    33. try {
    34. System.out.println("do method3..");
    35. Thread.sleep(2000);
    36. } catch (InterruptedException e) {
    37. e.printStackTrace();
    38. }
    39. }
    40. }
    41. public static void main(String[] args) {
    42. final ObjectLock objLock = new ObjectLock();
    43. Thread t1 = new Thread(new Runnable() {
    44. @Override
    45. public void run() {
    46. objLock.method1();
    47. }
    48. });
    49. Thread t2 = new Thread(new Runnable() {
    50. @Override
    51. public void run() {
    52. objLock.method2();
    53. }
    54. });
    55. Thread t3 = new Thread(new Runnable() {
    56. @Override
    57. public void run() {
    58. objLock.method3();
    59. }
    60. });
    61. t1.start();
    62. t2.start();
    63. t3.start();
    64. }
    65. }

    结果: 

     注:在多线程环境下使用 synchronized 代码块来实现不同类型锁的加锁操作,通过展示对象锁、类锁和任意对象锁的使用方式,说明了不同加锁方式在多线程并发环境中的作用,用法比较灵活。

     3.不要使用String的常量加锁,会出现死循环问题

    1. package com.ctb.sync6;
    2. /**
    3. * synchronized代码块对字符串的锁,注意String常量池的缓存功能
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class StringLock {
    10. public void method() {
    11. //new String("字符串常量")
    12. synchronized ("字符串常量") {
    13. try {
    14. while(true){
    15. System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
    16. Thread.sleep(1000);
    17. System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
    18. }
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }
    24. public static void main(String[] args) {
    25. final StringLock stringLock = new StringLock();
    26. Thread t1 = new Thread(new Runnable() {
    27. @Override
    28. public void run() {
    29. stringLock.method();
    30. }
    31. },"t1");
    32. Thread t2 = new Thread(new Runnable() {
    33. @Override
    34. public void run() {
    35. stringLock.method();
    36. }
    37. },"t2");
    38. t1.start();
    39. t2.start();
    40. }
    41. }

     结果:

    注:StringLock 类中,定义了一个 method() 方法,该方法使用 "字符串常量" 作为锁对象来实现同步操作。在方法中,通过 synchronized("字符串常量") 来对代码块进行加锁,确保多个线程在执行该代码块时是互斥的。

    main 方法中,创建了两个线程 t1 和 t2 分别执行 method() 方法。由于两个线程共享同一个字符串常量作为锁对象,因此它们在执行 method() 方法时会相互竞争这个锁。  

    1. public void method() {
    2. //new String("字符串常量")
    3. synchronized (new String("字符串常量")) {
    4. try {
    5. while(true){
    6. System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
    7. Thread.sleep(1000);
    8. System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
    9. }
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }

     结果:

    注:“字符串常量”它是只有一个引用,尽量不要拿字符串常量这种方式去加锁,我们可以使用new String("字符串常量"),注意即可  

    4.锁对象的改变问题

    当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同,如果对象本身不发生改变,那么依然是同步的,即使是对象的属性发生了改变。  

    1. package com.ctb.sync6;
    2. /**
    3. * 锁对象的改变问题
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class ChangeLock {
    10. private String lock = "lock";
    11. private void method(){
    12. synchronized (lock) {
    13. try {
    14. System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
    15. lock = "change lock";
    16. Thread.sleep(2000);
    17. System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. }
    23. public static void main(String[] args) {
    24. final ChangeLock changeLock = new ChangeLock();
    25. Thread t1 = new Thread(new Runnable() {
    26. @Override
    27. public void run() {
    28. changeLock.method();
    29. }
    30. },"t1");
    31. Thread t2 = new Thread(new Runnable() {
    32. @Override
    33. public void run() {
    34. changeLock.method();
    35. }
    36. },"t2");
    37. t1.start();
    38. try {
    39. Thread.sleep(100);
    40. } catch (InterruptedException e) {
    41. e.printStackTrace();
    42. }
    43. t2.start();
    44. }
    45. }

    结果:

    注:当我们使用字符串常量作为一把锁的时候,一定要注意在synchronized代码块里尽量不要去修改该锁对象的内容,第一个线程拿到的锁lock的值是lock,第二个线程拿到的是change lock去获得锁  

    1. private void method(){
    2. synchronized (lock) {
    3. try {
    4. System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
    5. //lock = "change lock";
    6. Thread.sleep(2000);
    7. System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }

    结果:

    注://lock = "change lock";当我们不去修改该锁对象的内容时,他将会依次去获取锁。  

    1. package com.ctb.sync6;
    2. /**
    3. * 同一对象属性的修改不会影响锁的情况
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class ModifyLock {
    10. private String name ;
    11. private int age ;
    12. public String getName() {
    13. return name;
    14. }
    15. public void setName(String name) {
    16. this.name = name;
    17. }
    18. public int getAge() {
    19. return age;
    20. }
    21. public void setAge(int age) {
    22. this.age = age;
    23. }
    24. public synchronized void changeAttributte(String name, int age) {
    25. try {
    26. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 开始");
    27. this.setName(name);
    28. this.setAge(age);
    29. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 修改对象内容为: "
    30. + this.getName() + ", " + this.getAge());
    31. Thread.sleep(2000);
    32. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 结束");
    33. } catch (InterruptedException e) {
    34. e.printStackTrace();
    35. }
    36. }
    37. public static void main(String[] args) {
    38. final ModifyLock modifyLock = new ModifyLock();
    39. Thread t1 = new Thread(new Runnable() {
    40. @Override
    41. public void run() {
    42. modifyLock.changeAttributte("张三", 20);
    43. }
    44. },"t1");
    45. Thread t2 = new Thread(new Runnable() {
    46. @Override
    47. public void run() {
    48. modifyLock.changeAttributte("李四", 21);
    49. }
    50. },"t2");
    51. t1.start();
    52. try {
    53. Thread.sleep(100);
    54. } catch (InterruptedException e) {
    55. e.printStackTrace();
    56. }
    57. t2.start();
    58. }
    59. }

     结果:

    注:由于 changeAttribute 方法使用了对象锁,因此在多线程环境下,只有一个线程能够执行该方法,保证了对同一个对象的属性修改操作是互斥的。一个对象里面的属性发生改变的时候,是不影响锁的变化的,还是这个对象。

    使用对象锁实现了对同一对象属性的修改操作的线程安全性,确保了多线程环境下对对象属性的修改操作是同步的,避免了数据不一致的情况发生  

    5.死锁问题  

    1. package com.ctb.sync6;
    2. /**
    3. * 死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况
    4. *
    5. * @author biao
    6. *
    7. * 2024年
    8. */
    9. public class DeadLock implements Runnable{
    10. private String tag;
    11. private static Object lock1 = new Object();
    12. private static Object lock2 = new Object();
    13. public void setTag(String tag){
    14. this.tag = tag;
    15. }
    16. @Override
    17. public void run() {
    18. if(tag.equals("a")){
    19. synchronized (lock1) {
    20. try {
    21. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");
    22. Thread.sleep(2000);
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. synchronized (lock2) {
    27. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");
    28. }
    29. }
    30. }
    31. if(tag.equals("b")){
    32. synchronized (lock2) {
    33. try {
    34. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入了lock2执行");
    35. Thread.sleep(2000);
    36. } catch (InterruptedException e) {
    37. e.printStackTrace();
    38. }
    39. synchronized (lock1) {
    40. System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入了lock1执行");
    41. }
    42. }
    43. }
    44. }
    45. public static void main(String[] args) {
    46. DeadLock d1 = new DeadLock();
    47. d1.setTag("a");
    48. DeadLock d2 = new DeadLock();
    49. d2.setTag("b");
    50. Thread t1 = new Thread(d1, "t1");
    51. Thread t2 = new Thread(d2, "t2");
    52. t1.start();
    53. try {
    54. Thread.sleep(500);
    55. } catch (InterruptedException e) {
    56. e.printStackTrace();
    57. }
    58. t2.start();
    59. }
    60. }

     结果:

    注:run() 方法中,如果 tag 的取值为 "a",则线程会先获取 lock1,然后尝试获取 lock2;如果 tag 的取值为 "b",则线程会先获取 lock2,然后尝试获取 lock1

    当两个线程同时运行时,它们会陷入死锁状态,因为彼此持有对方需要的锁而无法释放,导致程序无法继续执行下去,最终需要手动终止程序。

    为避免死锁,应该设计程序避免多个线程竞争多个锁的情况,或者确保多个锁的获取顺序是一致的,从而避免循环等待的情况发生。  

  • 相关阅读:
    软考中级软件设计师该怎么备考
    AndroidStudio案例——登录页面的切换
    QProgressDialog自动弹出的问题
    百货商场数字化会员营销 购物中心私域流量运营系统
    多极神经元手绘图作业,多极运动神经元手绘图
    2023秋招--梦加网络--游戏客户端--二面面经
    NoSQL - MongoDB 常见的shell操作 - 安装
    AVL树节点插入方式解析(单旋转和双旋转)
    Spring Cloud Function Spel表达式注入漏洞分析
    docker安装Prometheus+Grafana监控系统(SpringBoot集成监控)
  • 原文地址:https://blog.csdn.net/weixin_74268571/article/details/139704557