java线程安全常用手段总结

本文汇总了Java中常用的线程安全手段,包括线程封闭、让类不可变、使用volatile、加锁和CAS、安全发布对象、使用ThreadLocal以及线程安全的单例模式等,旨在帮助开发者理解和掌握多线程环境下保证线程安全的方法。

@[TOC](`java线程安全手段汇总)
#在前几篇文章中,从不同的维度介绍了java多线程开发相关的一些技术和原理实现,这里对前面几篇文章的内容进行一个汇总,总结java中我们常用的保证线程安全的手段。
什么是线程安全性?
在《Java 并发编程实战》中,定义如下:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
1.线程封闭:
实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并
发最简单的方法就是线程封闭。什么是线程封闭呢?
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。实现线程封闭有哪些方法呢?
ad-hoc 线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。Ad-hoc 线程封闭非常脆弱,应该尽量避免使用。
栈封闭
栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
无状态的类
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。
如果这个类的方法参数中使用了对象,也是线程安全的吗?比如:
在这里插入图片描述
当然也是,为何?因为多线程下的使用,固然 user 这个对象的实例会不正
常,但是对于 StatelessClass 这个类的对象实例来说,它并不持有 UserVo 的对象
实例,它自己并不会有问题,有问题的是 UserVo 这个类,而非 StatelessClass 本
身。
2.让类不可变
让状态不可变,两种方式:
1,加 final 关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上 final 关键字,但是加上 final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。但是要注意,一旦类的成员变量中有对象,上述的 final 关键字保证不可变并不能保证类的安全性,为何?因为在多线程下,虽然对象的引用不可变,但是对象在堆上的实例是有可能被多个线程同时修改的,没有正确处理的情况下,对象实例在堆中的数据是不可预知的这就牵涉到了如何安全的发布对象这个问题。
在这里插入图片描述
3.volatile
并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。
4.加锁和 CAS
我们最常使用的保证线程安全的手段,使用 synchronized 关键字,使用显式锁,使用各种原子变量,修改数据时使用 CAS 机制等等。
5.安全的发布
类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本;但是如果类中持有的成员变量是对象的引用,如果这个成员对象不是线程安全的,通过 get 等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
6.TheadLocal
ThreadLocal 是实现线程封闭的最好方法。ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,而 Map 的值就是我们要封闭的对象。每个线程中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的线程封闭。
【备注】:
Servlet 辨析
不是线程安全的类,为什么我们平时没感觉到:
1、在需求上,很少有共享的需求,2、接收到了请求,返回应答的时候,一
般都是由一个线程来负责的。
但是只要 Servlet 中有成员变量,一旦有多线程下的写,就很容易产生线程
安全问题。

并发下的性能:
使用并发的目标是为了提高性能,引入多线程后,其实会引入额外的开销,如线程之间的协调、增加的上下文切换,线程的创建和销毁,线程的调度等等。过度的使用和不恰当的使用,会导致多线程程序甚至比单线程还要低。
线程引入的开销:
1.上下文切换
如果主线程是唯一的线程,那么它基本上不会被调度出去。另一方面,如果可运行的线程数大于 CPU 的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用 CPU。这将导致一次上下文切换,在这个过程中将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。
2.内存同步
同步操作的性能开销包括多个方面。在 synchronized 和 volatile 提供的可见性保证中可能会使用一些特殊指令,即内存栅栏( Memory Barrier)。
内存栅栏可以刷新缓存,使缓存无效刷新硬件的写缓冲,以及停止执行管道。
内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序的。
3.阻塞
引起阻塞的原因:包括阻塞 IO,等待获取发生竞争的锁,或者在条件变量上等待等等。
如何减少锁的竞争:
减少锁的粒度
使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象。但是如果有同时要持有多个锁的业务方法,要注意避免发生死锁
缩小锁的范围
对锁的持有实现快进快出,尽量缩短持由锁的的时间。将一些 与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作
避免多余的锁
两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化—扩大锁的范围。
锁分段
ConcurrrentHashMap 就是典型的锁分段。
替换独占锁
在业务允许的情况下:
1、 使用读写锁,
2、 用自旋 CAS
3、 使用系统的并发容器
线程安全的单例模式
懒汉式
类初始化模式,也叫延迟占位模式。在单例类的内部由一个私有静态内部类来持有这个单例类的实例。
在这里插入图片描述
延迟占位模式还可以用在多线程下实例域的延迟赋值。
在这里插入图片描述
饿汉式
在声明的时候就 new 这个类的实例,因为在 JVM 中,对类的加载和类初始化,由虚拟机保证线程安全。
在这里插入图片描述
双重检查:
在这里插入图片描述

### Java线程安全的概念 在多线程环境中,当多个线程访问共享资源时,可能会发生数据不一致的情况。为了防止这种情况的发生,确保程序能够正确处理并发操作,就需要采用线程安全机制。 #### 线程安全的定义 如果某个对象能够在被多个线程使用的情况下始终表现出正确的行为,则该对象被认为是线程安全的。这意味着即使有多个线程同时调用其方法或修改其状态,也不会导致错误的结果或者破坏内部的数据结构[^1]。 ### 实现线程安全的方式 #### 使用同步代码块 最常用的一种方式是在关键区域加上`synchronized`关键字来声明一个临界区,在同一时刻只允许一个线程进入此区域执行: ```java public class Counter { private int count; public synchronized void increment() { this.count++; } } ``` 这种方法可以有效避免竞争条件,但是过度使用会影响性能,因为其他等待锁释放的线程会被阻塞住直到当前持有锁的操作完成为止[^2]。 #### 利用原子变量类 对于简单的计数器或者其他基本类型的更新操作来说,可以直接利用JDK提供的原子类型如`AtomicInteger`, `AtomicLong`等来进行无锁化的高效并发控制: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment(){ while(true){ int current = count.get(); int next = current + 1; if(count.compareAndSet(current, next)){ break; } } } } ``` 这种方式不仅提高了效率而且减少了死锁的风险[^3]。 #### 借助高级集合框架 除了上述两种基础手段外,还可以考虑使用Java Collections Framework中的并发版本容器比如`ConcurrentHashMap`、`CopyOnWriteArrayList`等等,这些容器已经内置了必要的同步逻辑从而简化开发人员的工作量并提高安全性[^4]。 ### 最佳实践建议 - **最小化锁定范围**:尽可能缩小加锁的作用域,减少不必要的争抢。 - **优先选用不可变对象**:一旦实例化之后就不能再改变的对象天然具有良好的线程特性。 - **合理选择工具库**:根据实际需求挑选合适的并发辅助设施而非盲目追求复杂度高的方案。 - **充分利用现有API**:现代Java提供了丰富的并发支持功能,应该充分理解和运用它们而不是重新发明轮子。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值