什么是线程安全

在《Java并发编程实战》中,定义如下:

当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在调用代码中不需要额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程不安全的原因

会从三方面进行考虑:就是原子性,可见性,有序性。在博客中会详细分析。

保证线程安全的手段有哪些

线程封闭

实现好的并发是一种困难的事。所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢?

就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的,就不会产生任何线程安全问题。(线程独占,局部变量)。

ad-hoc

简单来说,就是维护线程封闭性的职责完全由程序实现来承担。也就是靠我们程序员来搞定。它需要在没有任何一种语言特性,例如可见性修饰符或局部变量,能将共有对象封闭到目标线程上。因此ad-hoc线程封闭非常脆弱,应该尽量避免使用。

栈封闭

栈封闭是我们编程中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问同一个方法,此方法的局部变量都会进入线程独立的局部变量表中。所以局部变量是不被多个线程锁共享的,也就不会出现并发问题。所以能用局部变量就别用全局变量,全局变量容易引发并发问题。

无状态的类

没有任何成员的类,就叫无状态的类。这种类一定是线程安全的。

这个类中的某些方法参数中使用了对象也是线程安全的吗?如:

如何保证线程安全 Java 保证线程安全的方法_成员变量

因为在多线程环境下,UserVo如果不是并发安全的类,那么UserVo肯定有线程安全问题。当对于StatelessClass这个类的对象实例来说,它并不持有UserVo的对象实例(仅仅是方法的参数包含不算持有)。所以StatelessClass这个类自己不会有问题,有问题的是UserVo这个类(当然UserVo如果是无状态类也没问题)。

类不可变(意思一个线程也别改呗)

加final关键字

加final关键字,对于一个类,所有的成员变量都应该是私有的,同样的只要有可能,所有的成员变量都应该加上final关键字。但是加上final,要注意如果成员变量是对象时。这个对象锁对应的类也要是不可变的,才能保证整个类是不可变的。

如何保证线程安全 Java 保证线程安全的方法_多线程_02

不提供任何可修改成员变量的地方

不提供任何可修改成员变量的地方,同时也不作为方法的返回值。(不要有get,set方法)

如何保证线程安全 Java 保证线程安全的方法_多线程_03

Volatile

并不能保证类的线程安全性,只能保证类的可见性。最适合一个线程写,多个线程读的场景。(因为写和写之间并不能保证线程安全,但是可以保证获取数据的实时性)。

加锁和CAS

我们最常使用的保证线程安全的手段,使用synchronized关键字,使用显示锁。

使用各种原子变量,修改数据使用CAS机制等等。

如何安全的发布

原子性发布

类中持有的成员变量,如果是基本类型,发布出去并没有关系。因为发布出去的其实是这个变量的一个副本

如何保证线程安全 Java 保证线程安全的方法_线程安全_04

如何保证线程安全 Java 保证线程安全的方法_成员变量_05

以上就是一个基础类型int对外暴露的接口(发布),不涉及++操作,仅仅是每次get,set是不会有问题的。

非原子性发布

但如果是类中持有成员变量是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个对象本身持有的数据在多线程环境下不正确的修改。从而引发线程安全问题。

如何保证线程安全 Java 保证线程安全的方法_线程安全_06

如何保证线程安全 Java 保证线程安全的方法_多线程_07

JDK自带包装类

这个list发布出去后,是可以被不同的线程修改的。那么在多个线程同时修改的情况下线程不安全的问题是肯定存在的。怎么修正这个问题呢?我们在发布这个对象的时候,就应该用线程安全的方式来包装这个类。

如何保证线程安全 Java 保证线程安全的方法_多线程_08

对于我们自己使用或者声明的类,JDK自然没有提供这种包装类的方法。当时我们可以仿照这种模式或者委托给线程安全的类。当然这种通过get等方法发布出去的对象,最根本的解决办法还是应该在实现上就考虑到线程安全问题。如果这个类被封装好了不可见,那么经次包裹也可以使其线程安全。

错误的:自定义包装类

现有线程不安全的类:

如何保证线程安全 Java 保证线程安全的方法_成员变量_09

我们自己写一个类,将这个线程不安全的类传入,对每一个方法进行加锁增强。实现如下:

如何保证线程安全 Java 保证线程安全的方法_java_10

这样,我们可以保证每个线程进入不用的方法是互斥的,但是这样能保证线程安全么?

我们对整个对象中的所有元素,依次进行set操作。这本身就不是一个原子行为。

当有线程直接获取该对象时,是直接获取该对象的一个整体快照。而我设置对象的线程此时set进行了一半,势必会导致脏读。

那么想要解决这个问题,我们需要提供一个将所有对象打包设置的方法。

正确的:自定义包装类

如何保证线程安全 Java 保证线程安全的方法_java_11

对整个set进行打包加锁。让它具有原子性。

ThreadLocal

ThreadLocal是实现线程封闭的最好办法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,value是我们存入的对象。ThreadLocal利用Map实现了对象线程的封闭。

Servlet辨析

不是线程安全的类,为什么我们平时没感觉?

1. 需求上,很少有共享的需求。

2. 接收到请求,返回应答的时候一般都是由一个线程负责的。

所以Servlet一般数据都是局部,线程级变量。但如果Servlet中有成员变量,一旦有多线程的写操作,就很容易出现线程安全问题。