线程安全性

多线程程序中,如果控制不好,经常会出现各种的问题,有时候问题在调试或者测试的时候就会暴露出来,最要命的是程序部署一段时间之后才出现各种怪异现象,感到头疼?需要补充Java并发的知识了。该读书笔记系列以《Java并发编程实战》为基础,同时会参考网络上一些其他的资料,和大家一起学习Java并发编程的各个方面。这方面也是笔者比较薄弱的地方,理解不对的地方请留言或者邮件指出,同时也欢迎讨论。邮箱:[email protected]

线程安全性这章,将从如下几个方面入手,描述探讨:

1、什么是对象的状态

2、线程安全

3、无状态和有状态

4、竞态条件

5、加锁机制

内置锁

锁的重入

6、用锁来保护状态

7、小结

本章大部分是比较抽象的概念,不过没关系,我们将列举一些代码来逐一说明,这些概念上的认识,将会对以后理解有很大的帮助。

1、什么是对象的状态

书中指出,从非正式的意义上说,对象的状态是指存储在状态变量(实例或者静态域)中的数据,对象的状态可能包括其他的依赖对象的域,例如某个HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry中。在对象的状态中包含了任何可能影响其外部可见行为的数据。我们可以简单地理解为对象的状态就是对象的值和属性,对于简单的类型,就是其值。线程安全的代码,核心就是要对状态访问操作进行管理,特别是“共享的”和“可变的”状态。共享意味着多个线程可以同时访问,而可变的意思就是在其生命周期内,其值可以改变。

如果一个变量处于方法内部,它并不是共享的,因为每个线程执行代码的时候,都会有各自的值存储在线程的局部变量内,其他线程是无法访问的。如果一个类变量被声明成final的,它的属性也没有提供可访问的修改方法,那么它的状态就是不可变的。

2、线程安全

线程安全的核心就是正确性,正确性的含义是,某个类的行为与其规范完全一致。这里的规范可以理解为预期。

当多个线程访问某个类时,不需要添加任何的同步或者协同,这个类始终都能表现出正确的行为和结果,那么就称这个类是线程安全的。

可以这样理解,一个对象是否需要线程安全,取决于它是否被多个线程访问。这里指的是程序访问对象的方式,而不是对象要实现的功能。单线程访问任何状态都是安全的。如果多个线程访问一个未知线程安全的对象,就需要对访问方式进行控制。一个对象是否是线程安全的,指的是对象的实现本身就是线程安全的。这种情况下,不论对线程的访问是否做了控制,这个对象总是线程安全的。

线程安全的程序是否完全由线程安全的类组成?答案是否定的,完全由线程安全类构成的程序不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类。

所以线程安全其实就是在多线程环境下,类是否始终可以表现出正确的行为和结果,这个正确性其实是我们业务上的预期。

3、无状态和有状态

Servlet是多线程单实例的,这意味着多个多个请求共享一份Servlet对象,是一个典型的多线程访问的例子。Servlet分为无状态和有状态,分析如下的例子,本例子对书上的例子做了稍微的简化:

public class SafeServlet implements Servlet{

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
    }
…
}

 

此处省略了其他的一些方法,service方法从请求中获取到两个字符串,返回两个字符串的连接。

这个类是线程安全的,因为它是无状态的,它不包含任何域,也不包含任何其他类中域的引用,虽然所有线程都共享同一个SafeServlet实例,但是所有的线程都没有共享变量,每个线程都各行其是,没有交集,也不会相互影响。

所谓有状态,就像是我们在第一节提出来的,该对象存在着共享的变量,每个线程都可以访问这个变量。

像接下来的这个Servlet,用来统计处理次数:

public class UnsafeServlet implements Servlet{

    private long count = 0l;

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
        count ++;

    }
}

 

该Servlet是有状态的,不同的线程调用service时,都会处理一个共享的变量:count。

count操作并不是原子的,它可以分解为三个步骤:

读取count的值;

修改count的值;

写count的值;

这三步操作可能在多线程访问时完全搞乱顺序。当线程A刚读取完,线程B也读取了值,但是线程A的执行时间片(https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87)

完了,线程B理所当然地把count+1并且写入了count变量中。当线程A再次执行的时候,用到的count已经是过期的了。

并且其结果状态依赖上一个线程的处理。这会引发一系列不正确的结果。

无状态的类都是线程安全的,有状态的类,如果想线程安全,需要做一些并发控制。UnsafeServlet中的count,如果类型改成AtomicLong类型,这样可以把count++操作变成原子性的,因为AtomicLong对加一操作做了并发控制。由此我们可以想象,如果对状态的操作是原子性的,该对象也是线程安全的,当然方法不止这一种,后面将会提到。

4、竞态条件

这是维基百科的解释:https://zh.wikipedia.org/wiki/%E7%AB%B6%E7%88%AD%E5%8D%B1%E5%AE%B3

以上的UnsafeServlet中,由于不恰当的执行时序而出现的不正确的结果是非常典型的,我们称之为“竞态条件”。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会存在竞态条件。这种情况下是否返回正确的结果,完全靠运气。究其根本原因,就是可能基于一种已经失效的结果来做操作。

值得一提的是,设计模式:单例模式(https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F),很容易被写成线程不安全的如下方式:

public class Singlton {
    private Object obj = null;

    public Object getInstance(){
        if(obj == null){
            obj = new Object();
        }
        return obj;
    }
}

这里包含一个竞态条件,它可能破坏这个类的正确性。不难分析,当两个线程都访问这个类想获取一个Object对象的时候,A、B都判定obj是null。A、B都会创建一个Object对象。那么他们可能返回不同的对象。维基百科上给出了安全的处理方式,这里就不再赘述了。

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在在修改这个状态的过程中,也就是之前UnsafeServlet所描述的那样,将分步的操作复合成原子性的。实际情况中,尽可能能使用现有的线程安全对象来管理类的状态,这样更容易验证和维护线程的安全性问题。

5、加锁机制

UnsafeServlet的描述中,在Servlet中添加一个状态变量时,可以使用线程安全的对象来保证类的安全性,如果需要更多的状态变量时,是否只需要用线程安全的对象就可以保证线程安全了呢?不是这样的。

我们将代码稍作修改,添加一个变量记录最后一次请求的参数:

同样,省略了其他方法的实现。

public class UnsafeServlet implements Servlet{

    private AtomicLong count = new AtomicLong(0);
    private AtomicReference<String> lastParam1 = new AtomicReference<String>();

    public long getCount(){return count.get();}
    public String getLastParam1(){return lastParam1.get(); } 

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
        count.incrementAndGet();
        lastParam1.set(str1);
    }
…
}

然而这种方式并不正确,虽然我们的两个变量都是原子性的,也是线程安全的,但是这个类中存在竞态条件。

在线程安全的定义中,要求多个线程之间的操作无论采用什么执行时序或者交替方式,都要保证结果正确。虽然使用了set操作是原子性的变量,但是service方法无法保证两次set操作整体是原子性的。

内置锁

Java提供了一种内置机制来支持原子性:同步代码块(synchronized)。同步代码块包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。静态的synchronized方法以Class对象作为锁。

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁。

线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。无论是通过正常的控制路径退出还是通过代码块中抛出的异常退出,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

内置锁相当于一种互斥体,意味着当线程A进入同步代码块的时候,其他线程是无法进入代码块的,这样就可以保证代码块是原子性的。直到线程A释放锁,其他线程才能进入代码块。此处我们可以把整个servie方法都同步起来:

public synchronized void service(ServletRequest req, ServletResponse res)

但是这样的效率太低,因为这就相当于说明只能按顺序来访问该serlvet,违背了我们多线程访问的初衷,同样我们也可以只将操作属性的操作同步起来:

synchronized(this){count.incrementAndGet();lastParam1.set(str1);}

重入

重入指的是一种机制,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然后由于内置锁是可以重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求是会成功的。也就是说自己请求自己的锁,是可以成功的,这种机制避免了一些情况

下死锁的发生。

上面的代码中,子类改写了父类的synchronized方法,然后又调用父类中的方法,此时如果没有可重入的锁,那么这段代码将死锁。由于Widget和ChildWidget中doSth方法都是synchronized的,因此每个doSth方法在执行前都会获取Widget上的锁,因为这个锁已经被持有,而线程将永远等待下去。

public class Widget {
    public synchronized void doSth(){
        System.out.println("Parent Do Sth");
    }
}
public class ChildWidget extends Widget{
    @Override
    public synchronized void doSth() {
        super.doSth();
    }
}

6、用锁来保护状态

由于锁可以保护代码按串行的形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。共享状态的复合操作,如:递增、单例模式里先判断后创建对象等,都必须是以原子操作以避免竞态条件的产生。仅仅将复合操作封装到一个同步代码块中是不够的,如果用同步来协调对某个变量的访问,那么所有访问这个变量的位置都需要使用同一个锁。

对象的内置锁与其状态之间没有内在的关联,当获取与其对象相关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获取同一个锁。

之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象,如果自行构造一个锁对象,那么久需要在程序中自始至终都使用它们。

每个共享的可变的变量都应该只由一个锁在保护,从而使得维护人员知道是哪一个锁。

一种常见的约定是,将所有的可变状态都封装在对象内部,并且通过对象的内置锁对所有访问可变状态的代码进行同步。

7、小结

本节主要讲述了如下几个点,非正式的说法对象的状态就是对象的值和属性,对象在多线程访问时,如果总是能返回正确的结果,那么这个对象就是线程安全的,无状态的类一定是线程安全的,如果对象中存在竞态条件,将会出现多线程访问数据正确性问题。Java提供了内置锁来支持对象的线程安全。线程可以获取自己的锁,这就叫重入,由于锁机制只能保证同一个锁不被不同的线程持有,我们用锁机制来保护对象的状态时,需要注意不变性条件中的每个变量都要使用同一个锁来保护。

时间: 2024-08-11 07:48:41

线程安全性的相关文章

什么是线程安全性?

定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么久称这个类时线程安全的. 解释:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行, 并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施. 实例:一个无状态的Servlet @ThreadSafe public class SafeThreadTest implements Ser

JAVA并发编程实战 读书笔记(一)线程安全性

线程安全性   1.什么是线程安全 在线程安全的定义中,最核心的概念是正确性.正确性的含义是,某个类的行为与规范完全一致.当对正确性有了一个比较清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终能表现出正确的行为,那这个类就是线程安全的. 举例:无状态对象一定是线程安全的. 大多数Servlet都是无状态的,当Servlet在处理请求时需要保存一些信息时,线程安全才会成为一个问题. 2.原子性 举个例子:语句 ++i:虽然递增操作++i是一种紧凑的语法,使其看上去是一个操作,

java线程(二) - 线程安全性

前言: 要编写线程安全的代码,其核心在于要对状态访问的操作进行管理,特别是对共享的和可变的状态的访问. 当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,有三种方式可以修复这个问题: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 线程安全性的定义: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 那

VC和gcc在保证函数static变量线程安全性上的区别

VC和gcc不同,不能保证静态变量的线程安全性.这就给我们的程序带来了很大的安全隐患和诸多不便.这一点应该引起我们的重视!尤其是在构造函数耗时比较长的时候,很可能给程序带来意想不到的结果.本文从测试代码开始,逐步分析原理,最后给出解决方案. 多线程状态下,VC不能保证在使用函数的静态变量的时候,它的构造函数已经被执行完毕,下面是一段测试代码: class TestStatic { public: TestStatic() { Sleep(1000*10); m_num = 999; } publ

第二章:线程安全性——java并发编程实战

一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问状态变量时使用同步机制 完全由线程安全类构造的程序也不一定是线程安全的,线程安全类中也可以包含非线程安全的类 一.什么是线程安全性 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的.(因为线程安全类中封装了必要的同步代码) 一个无状态的类是线程安全的.无状态类是指不

并发编程初探-线程安全性

在Java并发编程中,对于线程安全是非常重要的,也是必须要考虑的一个问题.可以这么说,只要涉及到网络的,都必须考虑线程安全问题.好了,开始噼里啪啦地开始敲代码之前,我觉得有必要了解一些文绉绉的理论知识,因为这些理论知识是我们敲出来的代码是否是线程安全的一个依据. 当多个线程访问某个状态变量并且其中有一个线程执行写入操作的时候,必须考虑采用同步机制来协同这些线程对变量的访问,Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但"同步"这个术语还包括类型的变量,显

Java并发编程学习笔记(一)——线程安全性

1.当多个线程访问某个状态变量并且其中有一个献策灰姑娘执行写入操作时,必须采用同步机制来协同这些线程对变量的访问.Java中的主要同步机制是关键字synchronized,他提供了一种独占的加锁方式. 2.在任何情况下,只有当类中仅包含自己的状态时,线程安全类才是有意义的. 3.当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些献策灰姑娘讲如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 4.无状态对象一定是线程安全的

《Java并发变成实践》读书笔记---第二章 线程安全性

什么是线程安全性 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问.从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据."共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以发生变化.所以编写线程安全的代码更侧重于如何防止在数据上发生不受控的并发访问. 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误

并发基础知识 — 线程安全性

前段时间看完了<并发编程的艺术>,总感觉自己对于并发缺少一些整体的认识.今天借助<Java并发编程实践>,从一些基本概念开始,重新整理一下自己学过并发编程.从并发基础开始,深入进去,系统学习一下并发编程. 编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问.对象的状态是指存储在状态变量(实例或静态域)中的数据.对象的状态还可能包括其他依赖对象的域.(Map.Entry) 一个对象是否需要时线程安全的,取决于该对象

并发编程之线程安全性

一.什么是线程安全性 并发编程中要编写线程安全的代码,则必须对可变的共享状态的访问操作进行管理. 对象的状态就是存储在实例或者静态变量中的数据,同时其状态也包含其关联对象的字段,比如字典集合既包含自己的状态, 也包含KeyValuePair. 共享即可以多个线程同时访问变量,可变即变量在其声明周期内可以发生变化. 代码线程安全性关注的是防止对数据进行不可控的并发访问. 是否以多线程的方式访问对象,决定了此对象是否需要线程安全性.线程安全性强调的是对对象的访问方式,而不是对象 要实现的功能.要实现