当前位置: 首页 > article >正文

多线程2之线程同步

线程同步

线程同步的概念:

         线程同步是指多个线程在访问共享资源时,通过一定的机制来协调它们的执行顺序和访问方式,以确保共享资源在同一时刻只能被一个线程访问,或者在多个线程访问时能够保证数据的一致性和正确性。在多线程编程环境中,由于线程的执行是并发的,如果没有适当的同步机制,可能会导致数据竞争(data race)、死锁(deadlock)等问题。

为什么要线程同步:

数据竞争问题

        当多个线程同时访问和修改共享数据时,由于线程执行的顺序是不确定的,可能会导致数据不一致。例如,假设有两个线程同时对一个共享变量count进行自增操作。如果没有同步机制,可能会出现以下情况:线程 1 读取count的值为 5,在它即将写入新值(6)之前,线程 2 也读取了count的值为 5,然后两个线程分别将自己计算的值(6)写入count,最终count的值为 6,而不是正确的 7。

死锁问题

        在多个线程互相等待对方释放资源的情况下,会导致程序无法继续执行,形成死锁。例如,线程 A 持有资源 R1 并且等待资源 R2,而线程 B 持有资源 R2 并且等待资源 R1,这样两个线程都无法继续执行,程序陷入僵局。

线程同步之Synchronized

        Synchronized 是 Java 中的一个关键字,用于实现方法或代码块的同步。它提供了一种简单且有效的方式来控制多个线程对共享资源的访问,确保在同一时刻只有一个线程能够执行被 Synchronized 修饰的方法或代码块。其底层原理是基于对象的内部锁(也称为监视器锁,monitor lock)。

Synchronized的两种用法

1.修饰方法:

        public class SynchronizedMethodExample {
            private int count = 0;
            public synchronized void increment() {
                count++;
            }
            public int getCount() {
                return count;
            }
        }

increment方法被 Synchronized 关键字修饰。当一个线程调用increment方法时,它首先需要获取对象的内部锁。如果此时没有其他线程持有该锁,那么这个线程就可以顺利执行increment方法中的代码,即对count变量进行自增操作。如果有其他线程已经持有了这个锁,那么当前线程将被阻塞,直到持有锁的线程释放锁。这样就保证了在任何时候只有一个线程能够执行increment方法,从而避免了多个线程同时修改count变量可能导致的数据竞争问题。

2.修饰代码块:

        public class SynchronizedBlockExample {
            private Object lockObject = new Object();
            private int count = 0;
            public void increment() {
                synchronized (lockObject) {
                    count++;
                }
            }
            public int getCount() {
                return count;
            }
        }

这里使用 Synchronized 关键字修饰了一个代码块,并且指定了一个锁对象lockObject。当一个线程进入这个被 Synchronized 修饰的代码块时,它需要获取lockObject的锁。这种方式比直接修饰方法更加灵活,因为可以根据具体的需求选择不同的锁对象。例如,如果有多个方法需要对不同的共享资源进行同步操作,就可以分别为它们指定不同的锁对象,而不是将整个方法都进行同步。 

Synchronized 的优缺点

优点
简单易用
:作为 Java 内置的同步机制,使用 Synchronized 关键字来实现线程同步非常方便,不需要额外引入复杂的库或工具。对于初学者或者简单的多线程同步场景,很容易理解和掌握。
语义清晰:其语义明确,表示在同一时刻只有一个线程能够执行被修饰的方法或代码块,能够有效地避免数据竞争,保证数据的一致性和正确性。
缺点
灵活性相对较差:虽然可以通过修饰代码块来指定锁对象,但在一些复杂的场景下,可能需要更精细的锁控制策略。例如,对于读多写少的场景,Synchronized 无法区分读操作和写操作,所有的访问都需要获取相同的锁,这可能会导致不必要的性能损耗。
性能问题:在高并发的场景下,尤其是多个线程竞争激烈时,Synchronized 的性能可能会受到影响。因为当锁升级为重量级锁后,涉及到操作系统的内核态和用户态的切换,这种切换会带来较大的开销。而且,当一个线程持有锁时,其他等待的线程会被阻塞,不能有效地利用 CPU 资源。

线程同步之Lock

  • Lock 接口概述

    • 在 Java 中,Lock接口提供了比synchronized关键字更灵活的线程同步机制。Lock接口位于java.util.concurrent.locks包中,它定义了一系列用于控制多个线程对共享资源访问的方法。与synchronized基于对象内部锁不同,Lock接口通过显式的锁操作来实现线程同步,提供了更多的功能和更高的灵活性。
  • Lock 接口的主要实现类 - ReentrantLock

    可重入性

  • ReentrantLockLock接口最常用的实现类,它具有可重入性。这意味着一个线程可以多次获取同一个锁。例如,如果一个线程在一个已经获取了ReentrantLock锁的方法中调用了另一个同样需要获取该锁的方法,线程不会被阻塞,而是可以成功获取锁,就像使用synchronized关键字时的情况一样。
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class LockExample {
        private final Lock lock = new ReentrantLock();
        private int count = 0;
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
        public int getCount() {
            return count;
        }
    }

首先创建了一个ReentrantLock对象lock。在increment方法中,通过lock.lock()来获取锁,然后在try - finally块中执行对共享资源(count变量)的操作。在finally块中,通过lock.unlock()来释放锁。这种方式确保了无论在操作共享资源的过程中是否发生异常,锁都会被释放,从而避免了死锁的情况。

Lock 接口提供的其他功能

  • 公平锁与非公平锁
    • 公平锁(Fair Lock)ReentrantLock可以通过构造函数来指定是否为公平锁。公平锁遵循先来先得的原则,即等待时间最长的线程会最先获取锁。例如,在一个多线程环境下,如果有多个线程在等待获取锁,那么这些线程会按照请求锁的先后顺序来获取锁。公平锁可以保证线程获取锁的顺序性,但可能会导致性能下降,因为需要维护一个等待队列,每次释放锁时都要检查队列中是否有等待时间最长的线程。
    • 非公平锁(Unfair Lock):默认情况下,ReentrantLock是一个非公平锁。非公平锁在释放锁后,不会考虑等待线程的等待时间,任何一个等待线程都有可能获取到锁。这样可能会导致某些线程长时间等待,但在高并发场景下,非公平锁通常比公平锁具有更好的性能,因为它减少了线程切换和等待队列维护的开销。
  1. Lock 与 Synchronized 的比较

    • 灵活性Lock接口提供了更多的功能,如公平锁 / 非公平锁的选择、可中断锁以及更灵活的条件变量。而synchronized关键字相对简单,使用时基于对象的内部锁,没有这些额外的功能选项。
    • 性能:在低并发场景下,synchronizedLock的性能差异不大。但在高并发场景下,ReentrantLock(非公平锁)可能会因为减少了一些不必要的等待队列维护和线程切换开销而具有更好的性能。不过,synchronized在 JDK 的不断优化下,性能也在逐步提升。
    • 使用的便利性synchronized关键字使用起来更加简单,它是 Java 语言的内置特性,语法简洁,对于简单的线程同步场景,代码量较少。而Lock接口需要显式地进行锁的获取和释放操作,代码结构相对复杂一些,但在复杂的多线程场景下,能够提供更精细的控制。

http://www.kler.cn/a/375636.html

相关文章:

  • 大数据新视界 -- 大数据大厂之数据质量管理全景洞察:从荆棘挑战到辉煌策略与前沿曙光
  • 微信小程序中,点击视频,没有跳转播放,可能是因为没有在app.json中正确注册视频播放页面的路径
  • 显示器接口
  • Unity控制物体透明度的改变
  • [供应链] 邀请招标
  • GIN 反向代理功能
  • 考研要求掌握的C语言(二叉排序树专题)
  • blender 小车建模 建模 学习笔记
  • C++之多态(上)
  • [实时计算flink]CREATE TABLE AS(CTAS)语句
  • 部署服务器监控集群之“Prometheus Grafana Alertmanager“
  • 智慧城市:未来城市的蓝图
  • Jupyter Notebook 打开指定文件夹
  • Straightforward Layer-wise Pruning for More Efficient Visual Adaptation
  • 用示波器如何测量信号的相位差?
  • 鸿蒙系统不断发展的看法
  • Python实现Lucas-Lehmer测试
  • Android 滴滴面经
  • No.22 笔记 | WEB安全 - 任意文件绕过详解 part 4
  • 深入理解数据库的三范式
  • OpenCV—HoughLines中的theta角度理解
  • ArcGIS Pro SDK (二十一)渲染
  • CSP/信奥赛C++刷题训练:经典差分例题(3):洛谷P5542 :[USACO19FEB] Painting The Barn S
  • fastboot相关的命令大全
  • 计算机后台服务-更新下载,重启————未来之窗行业应用跨平台架构
  • Notepad++检索包含多个关键字的行