一、聊聊并发—线程安全到底在说什么

1,337 阅读7分钟

前言

并发编程的目的是为了让程序运行得更快,提高程序的响应速度,虽然我们希望通过多线程执行任务让程序运行得更快,但是同时也会面临非常多的挑战,比如像线程安全问题、线程上下文切换的问题、硬件和软件资源限制等问题,这些都是并发编程给我们带来的难题。其中线程安全问题是我们最关心的问题之一,我们接下来主要就围绕着线程安全的问题来展开。

线程安全性

首先我们要明白,要如何界定线程安全和线程不安全,我查找了很多资料,没能找到一个我认为权威又严谨的定义来界定它们,不过我觉得有一个概念可以帮助我们来区分线程安全和非安全:竞态关系。

那什么叫竞态关系呢。当多个线程同时访问同一个资源,如果这个共享资源对访问顺序敏感,程序的输出结果会严重依赖对事序的致命相依性,这个时候多个线程之间就存在竞态关系,当存在竞态关系的时候,此时程序的执行结果有很多不确定性,也就是说程序运行的结果全凭运气。

那是不是存在竞态关系的线程一定不安全,不存在竞态关系的线程之间一定安全呢?在没有任何其他约束的条件下,不添加额外加的同步,线程间存在竞态关系,那一定不是线程安全的;如果不存在竞态关系,那它们一定是线程安全的。因为竞态关系是发生在共享的资源上,如果没有竞态关系说明了不会对共享资源同时访问,也就不存在线程安全的问题了。

在《Java并发编程实战》一书中给出了线程安全的定义:当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为。而且这本书第一章的开头就这样写道:要编写线程安全的代码,其核心是要对操作访问的管理。其实就是为了防止共享状态在并发访问的时候发生不可控状态,所以对于在线程中共享的那些状态一定要引起我们格外的注意。

Tips:

共享的和可变的状态 这个一定要记牢,这是线程安全的核心

Java线程间消息传递方式

在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

  • 共享内存:共享内存的模型是使用比较多的一种模型。这种通讯模型通过设置一个共享变量,多线程之间通过操作同一个变量的方式达到通讯的目的。但是这种方式就需要我们在操作共享变量的地方或者代码片段中,显式指定线程之间互斥执行。
  • 消息传递:消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

多线程的通信大部分是通过共享内存来进行的,在Java中多线程的通信方式也是采用共享内存,但是这种方式是有弊端的,这种通信方式其实是线程之间通过写-读内存中的公共状态来隐式进行通信,那多线程之间是如何进行公共状态的内存读写,我们没办法显式的看到,所以这就需要我们了解多线程对于内存的读写机制。

Java多线程的内存交互

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存。线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对共享变量的所有操作都必须在工作内存中进行,不能直接操作主内存变量,而是将变量拷贝到本地内存中,在本地内存操作完成以后,再将结果同步回主内存,不同的线程之间也无法直接访问对方工作内存中的变量。如下图所示:

221.png
221.png

当线程A需要向线程B发送消息时,首先A通过拷贝主内存中的变量到自己的本地内存中,在本地内存进行处理,处理完成以后,将自己本地内存中的数据同步到到主内存。线程B将线程A同步到主内存中的变量拷贝到自己的本地线程,然后完成自己的处理,如此往复的进行,线程A和线程B就完成了消息的传递。

在这里主要想说ava多线程之间是如何通过共享内存来进行消息传递的,以及多线程和共享变量之间的交互方式。这里我们就对Java内存模型混个眼熟,就先不介绍Java内存模型是什么了,避免给读者们增加负担,我会在接下来的文章中进行详细的介绍,毕竟内存模型也是并发编程中比较重要的一部分内容。

并发带来的问题

通过上面的介绍我们可能了解多线程之间通信的方式,但是这种方式也会带来两个问题:可见性和访问问题。

因为线程之间都是通过访问主内存来进行数据交换的,那假如线程A先读取了某些共享数据,之后线程B对这些数据进行了修改,那么线程A可能看不到线程B对这数据的改动。

当线程A和线程B同时对这个共享数据做出修改时,到底是A线程数据为准还是B线程数据。

除此之外呢,还会带来原子性、重排序问题,这个我们后面的文章会详细的进行介绍。

绝对的线程安全

我们之前说过,线程安全是和可变的共享变量有关系,那如果没有了共享变量或者共享变量不可变,是不是这个类就是绝对的线程安全了?答案是的。这也我们所说的不可变对象和无状态对象,这两种对象一定是线程安全的。

无状态对象

无状态对象,它既不包含任何域,也不包对其他类中域的引用,计算过程中的临时状态仅存于线程栈上,只能由当前线程访问。我们可以认为无状态变量是没有共享状态的,所以是线程安全的。Java中Servlet对象,就是一个无状态的对象。

public class ServletTest implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException { }

public ServletConfig getServletConfig() {return null; }

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { }

public String getServletInfo() { return null;}

public void destroy() { }
}

不可变对象

不可变对象它只有一种状态,而且这种状态由构造函数来控制,不可变对象一旦被创建完成,就没有办法修改。既然它只有一种状态,那就不存在改变的可能,所以不可变对象一定是线程安全的。但是不可变对象,不是所有的域是声明为final类型,它就是不可变的。

当满足以下条件是,对象才是不可变的:

  • 对象被创建以后,其状态就不能改变
  • 对象的所有域都是final类型。
  • 对象是被正确创建的。因为多线程中拿到的对象可能不是一个构建完整的对象。

总结

看完上面的内容,我们可能对并发编程有一个大概的了解。

  1. 并发编程的多线程安全问题和共享可变状态有关系
  2. Java中多线程的通信方式是通过共享内存来进行的。
  3. 无状态、不可变对象一定是线程安全的。
  4. 并发编程带来的原子性、可见性、重排序问题

参考:

《Java并发编程实战》