线程安全和线程同步

概述

线程安全和线程同步是并发编程中的两个重要概念,它们有助于程序员确保多线程环境下程序的正确性和稳定性。

线程安全:线程安全是指多线程环境下,程序能够正确、稳定地运行,不会出现数据不一致或者其他不可预料的结果。换句话说,线程安全的代码可以在多线程环境中无冲突地执行,每个线程都能得到正确的结果。
实现线程安全的主要方法包括:

  1. 避免共享状态:尽量让每个线程只操作自己的数据,避免多线程共享数据。
  2. 使用同步机制:当必须共享数据时,使用同步机制(如锁)来确保数据访问的原子性和顺序性。
  3. 使用线程安全的集合和类:Java等语言提供了许多线程安全的集合和类,可以直接使用。

线程同步:线程同步是协调多个线程之间的执行顺序,以确保它们能够正确地共享资源。在多线程环境中,线程之间可能会争夺对共享资源的访问权,如果没有适当的同步机制,就可能导致数据不一致或其他问题。
线程同步的主要方法包括:

  1. 使用锁:通过获取和释放锁来控制对共享资源的访问。当一个线程持有锁时,其他线程无法访问该资源,直到锁被释放。
  2. 使用条件变量:条件变量允许线程在某些条件不满足时等待,当条件满足时被唤醒。这有助于实现线程之间的协作。
  3. 使用信号量:信号量是一种用于控制多个线程访问共享资源的计数器。它允许一定数量的线程同时访问资源,当计数器达到零时,其他线程必须等待。
    线程安全和线程同步是相辅相成的。线程同步是实现线程安全的一种手段,而线程安全则是线程同步的目标。在多线程编程中,我们需要根据具体的需求和场景来选择合适的同步机制,以确保程序的正确性和稳定性。

线程安全

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况

不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
不可变对象天生是线程安全的
线程安全是指多线程环境下,当多个线程同时访问共享资源时,保证对该资源的操作能够正确地执行,并且不会出现意外的结果或破坏数据的一致性。
在多线程编程中,线程安全是一个重要的概念,因为多个线程并发地访问共享资源可能会引发竞争条件(Race Condition)和其他并发问题,导致程序运行出现错误或结果不符合预期。
出现线程安全问题的原因
在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写入已经开始,但还没结束),其他线程对这个写了一半的资源进行了读操作,或者对这个写了一半的资源进行了写操作,导致此资源出现数据错误。

如何避免线程安全问题

● 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。
● 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。
简单来说,在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线程如何去交替执行。在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能按照预期的结果来反馈,那么这个类就是线程安全的。实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性。
原子性,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断的,因为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前后执行结果不一致的问题。这个和数据库里面的原子性是一样的,简单来说就是一段程序只能由一个线程完整的执行完成,而不能存在多个线程干扰。CPU 的 上 下 文 切 换 , 是 导 致 原 子 性 问 题 的 核 心 , 而 JVM 里 面 提 供 了Synchronized 关键字来解决原子性问题。
可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的。导致可见性问题的原因有很多,比如 CPU 的高速缓存、CPU 的指令重排序、编译器的指令重排序。
有序性,指的是程序编写的指令顺序和最终 CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。
可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决。在我看来,导致有序性、原子性、可见性问题的本质,是计算机工程师为了最大化提升 CPU 利用率导致的。
比如为了提升 CPU 利用率,设计了三级缓存、设计了 StoreBuffer、设计了缓存行这种预读机制、
在操作系统里面,设计了线程模型、在编译器里面,设计了编译器的深度优化机制。

实现线程安全的方法

  1. 互斥访问:使用锁(如synchronized关键字、Lock接口)来保护共享资源,确保同一时间只有一个线程能够访问该资源。通过互斥访问,可以避免多个线程同时修改共享资源而产生的数据不一致问题。
  2. 原子操作:使用原子操作(Atomic Operation)来确保某些操作的原子性,即这些操作在多线程环境下不可被中断。原子操作可以保证对共享资源的读取、修改等操作是线程安全的。
  3. 不可变对象:采用不可变对象来确保线程安全。不可变对象在创建后不能被修改,因此多个线程访问同一个不可变对象不会引发竞争条件。
  4. 线程局部存储(Thread-Local Storage):使用线程局部存储来为每个线程提供独立的变量副本,确保每个线程在操作变量时不会相互干扰。
  5. 并发容器:Java提供了一些并发容器(如ConcurrentHashMap、ConcurrentLinkedQueue等),这些容器通过内部的线程安全机制,可以在多线程环境下安全地进行操作。
    加锁,比如悲观锁 select for update,sychronized等,如,乐观锁,乐观锁如
    CAS等,还有 redis分布式锁等等。
    值得注意的是,虽然使用上述方法可以提高线程安全性,但并不能保证程序在所有情况下都是完全线程安全的。在编写多线程程序时,还需要结合具体场景和需求,仔细分析并发访问共享资源可能产生的问题,并采取适当的线程安全措施。

servlet 是线程安全吗

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能 够正确地处理多个线程之间的共享变量,使程序功能正确完成。 Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问 同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理 流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程 安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来 处理多线程的问题。

线程同步

用 synchronized 关键字
wait 和 notify
使用特殊域变量 volatile 实现线程同步
使用重入锁实现线程同步
使用局部变量来实现线程同步
使用阻塞队列实现线程同步
使用原子变量实现线程同步
CountDownLatch:允许一个或多个线程等待其他线程执行完毕之后再执行,可以用于线程之间的协调和通信。
CyclicBarrier类:允许多个线程在一个栅栏处等待,直到所有线程都到达栅栏位置之后,才会继续执行。
Semaphore:允许多个线程同时访问共享资源,但是限制访问的线程数量。可以用于控制并发访问的线程数量,避免系统资源被过度占用。

线程同步是为了保证多个线程之间的有序执行和数据的一致性。以下是几种常见的线程同步方式:

  1. 锁机制:使用锁机制是最常见的线程同步方式之一。可以使用内置锁(synchronized 关键字)或显式锁(ReentrantLock 类)来实现线程间的互斥访问。
  2. 互斥量:互斥量是一种进程间或线程间的同步原语,通过对共享资源加锁来确保一次只有一个线程访问共享资源。
  3. 条件变量:条件变量用于线程间的等待和通知机制。一个线程可以等待某个条件满足,而另一个线程在某个条件满足时进行通知。
  4. 信号量:信号量是一种经典的线程同步原语,用于控制同时访问某个资源的线程数。
  5. 屏障(Barrier):屏障用于保证多个线程在某个点上同步,只有当所有线程都达到屏障点时,它们才能继续执行。
  6. 原子操作:原子操作是不可中断的单个操作,可以确保多个线程对共享变量的并发访问是线程安全的。
  7. 读写锁:读写锁(ReadWriteLock)允许多个线程同时读取共享资源,但在有线程写入时会阻塞其他线程。
    以上是常见的几种线程同步方式,每种方式都有其适用场景和特点。在选择线程同步方式时,需要根据具体的需求、并发性能要求和线程安全性等因素进行权衡和选择。

1.同步方法

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如: public synchronized void save(){}注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2.同步代码块

即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

3.使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。
用final域,有锁保护的域和volatile域可以避免非同步的问题。
4.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
注:关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制,
能够帮助用户处理所有与锁相关的代码。
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
5.使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
6.使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
本小节主要是使用LinkedBlockingQueue来实现线程的同步
LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO)
LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞

7.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
多线程同步机制
在需要同步的方法的方法签名中加入synchronized关键字。
使用synchronized块对需要进行同步的代码段进行同步。
使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值