【多线程学习笔记4】线程同步

简介: 【多线程学习笔记4】线程同步

当使用多线程来访问同一个数据时,就会很容易出现线程安全问题,最经典的问题就是银行取钱的问题。而如何处理线程安全问题呢,最常用的回答就是加锁,实现线程同步。那么如何加锁呢,拆分之后,无非是下面三种方式:同步代码块、同步方法、同步锁。


1 同步代码块


同步代码块的语法如下:

synchronized(obj){
    //此处的代码就是同步代码块
}


上述语法格式中的synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。


注意:阻止多线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。


2 同步方法


与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。


注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。


可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采取如下策略:


(1)不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。


(2)如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。


JDK所提供的StringBuilder、StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBulider来保证较好的性能;当需要保证多线程安全时,就应该使用SringBuffer。


释放同步监视器的锁定:


针对同步代码块、同步方法,程序无法显式释放对同步监视器的锁定,那么线程在哪几种情况下会释放同步监视器的锁定呢?


(1)同步方法、同步代码块执行结束;

(2)同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行;

(3)同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;

(4)程序执行了同步监视器对象的wait()方法。


下面的情况不会释放锁定:程序调用sleep()、yield()和suspend()方法。


3 同步锁


从Java5开始,Java还提供了一种功能强大的线程同步机制,通过显示定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。


Lock是控制多线程对共享资源进行访问的工具,通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。


某些锁可能允许对共享资源并发访问,如ReadWriteLock,另外还为Lock提供了ReentrantLock(可重入锁)实现类。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:

class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    public void m(){
        //加锁
        lock.lock();
        try{
            //需要保证线程安全的代码
            // method body
        }
        //使用finally块来保证释放锁
        finally{
            lock.unlock();
        }
    }
}


注意:


(1)使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。


(2)使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。


(3)ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。


4 死锁


当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施处理死锁的情况,所以在多线程编程时,应该采取措施避免死锁出现。而一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态。

相关文章
|
23天前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
59 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
26天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
10天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
12 3
|
10天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
10 2
|
10天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
22 2
|
10天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
22 1
|
10天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
22 1
|
10天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
18 1
|
26天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
42 6
|
23天前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
36 1