`
vitoer
  • 浏览: 6306 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Java进阶(二)当我们说线程安全时,到底在说什么

 
阅读更多
本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/java/thread_safe/

多线程编程中的三个核心概念
原子性
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

可见性
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。

CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。

顺序性
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。

以下面这段代码为例

1
2
3
4
boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。

处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

讲到这里,有人要着急了——什么,CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,大家大可放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

Java如何解决多线程并发问题
Java如何保证原子性
锁和同步

常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。

1
2
3
4
5
6
7
8
9
public void testLock () {
  lock.lock();
  try{
    int j = i;
    i = j + 1;
  } finally {
    lock.unlock();
  }
}
与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例

1
2
3
4
5
6
public void testLock () {
  synchronized (anyObject){
    int j = i;
    i = j + 1;
  }
}
无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。

CAS(compare and swap)

基础类型变量自增(i++)是一种常被新手误以为是原子操作而实际不是的操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下。

1
2
3
4
5
6
7
8
AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}
Java如何保证可见性
Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

Java如何保证顺序性
上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。

Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。

synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式的保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。

happens-before原则(先行发生原则)
传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
程序次序规则:一个线程内,按照代码顺序执行
线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
线程终结原则:线程的终止检测后发生于线程中其它的所有操作
线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
对象终结规则:一个对象构造先于它的finalize发生
volatile适用场景
volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
boolean isRunning = false;
public void start () {
  new Thread( () -> {
    while(isRunning) {
      someOperation();
    }
  }).start();
}
public void stop () {
  isRunning = false;
}
在这种实现方式下,即使其它线程通过调用stop()方法将isRunning设置为false,循环也不一定会立即结束。可以通过volatile关键字,保证while循环及时得到isRunning最新的状态从而及时停止循环,结束线程。

线程安全十万个为什么
问:平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
答:锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。


问:锁和synchronized为何能保证可见性?
答:根据JDK 7的Java doc中对concurrent包的说明,一个线程的写结果保证对另外线程的读操作可见,只要该写操作可以由happen-before原则推断出在读操作之前发生。

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

问:既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
答:synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

问:既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
答:锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。

问:还有没有别的办法保证线程安全
答:有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。

问:synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。
分享到:
评论

相关推荐

    java多线程进阶

    这本书“java多线程进阶”显然旨在帮助读者深化这方面的理解,打通编程中的“任督二脉”,使开发者能够更加熟练地在并发环境中编写高效且稳定的代码。 1. **线程基础**:书中首先会介绍Java多线程的基础知识,包括...

    JAVA零基础到高级进阶特训营 JAVA多线程并发设计+Spring高级+数据库开发+JAVA基础等

    这套课程既可以作为从零基础开始...课程的主要内容涉及有JAVA基础课程、JAVA多线程与并发编程、数据库开发基础和进阶、Spring Framework、Spring进阶、Spring MVC框架、Spring boot、Java常用类库、Java异常处理等等

    推荐优质Java课程 疯狂Java语言编程 Java入门到进阶教程 16.多线程(共44页).ppt

    总的来说,这个系列的Java教程全面覆盖了从基础到高级的Java编程技能,尤其在多线程编程这一领域,不仅教授了基本概念,还深入到实践应用,对学习者提升Java编程能力大有裨益。通过系统学习,开发者可以具备开发复杂...

    Java基础/进阶书籍

    对于想要在Java领域进一步提升的人来说,这三本书无疑是不可多得的学习资源。同时,它们也将帮助你应对实际工作中可能出现的各种问题,提高代码质量和性能,使你成为一名更加熟练和自信的Java开发者。

    Java进阶路线

    ### Java进阶路线详解 #### 一、Java基础 **1. 传值与传引用** 在Java中,基本类型(如int、char等)的传递是按值传递的,而对象类型的传递则是按引用传递的。理解这一点对于正确处理变量和对象之间的交互至关...

    java高级进阶知识

    《深入理解Java虚拟机:JVM高级特性与最佳实践》是Java开发者深入探索JVM的必备书籍,尤其对于想要在Java领域进一步提升的程序员来说,它提供了丰富的理论基础和实践经验。这本书的第二版更是对原有的内容进行了更新...

    java大神进阶之路.pdf

    【标题】Java大神进阶之路 【描述】 Java大神进阶之路是一份针对Java开发者进阶提升的学习指南,详细描述了从编程基础到Java语言本身的精通,再到数据库以及Java Web核心技术的掌握,以及每个阶段所需要学习和掌握...

    java基础与进阶资料

    Java是一种广泛使用的面向对象的编程语言,以其跨平台、健壮性和安全性著称。"Java基础与进阶资料"这个标题暗示了这份压缩包中包含的内容涵盖了从Java的基础概念到高级特性的学习材料,旨在帮助开发者从入门到精通...

    Java语言程序设计进阶篇答案与代码

    本资源包含了教材中的所有源代码和相关复习题、编程题的答案,这对于自学Java或者正在学习Java进阶知识的人来说是一份宝贵的资料。 在Java语言中,进阶篇通常会涉及以下几个核心知识点: 1. **多线程**:Java提供...

    《Java语言程序设计(进阶篇)》 课后习题第25章代码chapter25.rar

    以下将详细介绍第25章可能涉及的知识点,并根据通常的Java进阶内容进行扩展。 第25章可能涵盖的主题包括但不限于: 1. **多线程**:Java提供了强大的多线程支持,这可能是第25章的重点。学生可能需要理解和掌握`...

    java基础学习与进阶

    对于初学者来说,Java基础学习是进入这个领域的第一步。本文将深入探讨Java的基础知识和进阶概念,帮助零基础的同学逐步掌握这门语言。 首先,我们要理解Java的基本语法。Java程序由类(class)组成,每个类都包含...

    Java 7编程高级进阶源代码

    Java 7编程高级进阶源代码是一份宝贵的资源,它为深入理解和实践Java 7的高级特性提供了丰富的实例。这份源代码集包含了书本中提到的各种复杂编程概念和技术的实现,旨在帮助Java开发者提升技能,从初级阶段跃升至...

    java集合框架全面进阶

    7. **并发处理**:在多线程环境中,Concurrent包提供了线程安全的集合实现,如ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet。它们在保证线程安全的同时,尽可能减少同步开销。 8. **泛型和类型...

    java进阶笔记.zip

    这些"java进阶笔记"涵盖了许多核心的Java概念,对于深入理解和提升Java技能至关重要。让我们逐一探讨这些主题。 首先,"Collection"是Java集合框架的基础,它定义了存储一组对象的基本接口。在Java中,集合分为两大...

    Java语言程序设计进阶篇(第5版)源代码

    《Java语言程序设计进阶篇(第5版)》是一本深入探讨Java编程技术的权威教材,适合已经掌握Java基础的开发者进一步提升技能。这本书的源代码提供了丰富的实例,涵盖了多线程、网络编程、I/O流、集合框架、异常处理、...

    Java语言程序设计 进阶篇(原书第8版).pdf

    总之,《Java语言程序设计 进阶篇(原书第8版)》是一本全面覆盖Java高级特性的教程,对于希望提升编程技能、深入理解Java内在机制的开发者来说,具有极高的参考价值。通过阅读和实践书中的例子,读者可以掌握高级...

    Java语言程序设计.进阶篇.原书第10版

    3. **反射机制**:Java反射API允许程序在运行时动态地获取类的信息并操作类的对象。书中会涉及Class类、Constructor类、Method类和Field类的使用,以及动态代理的实现。 4. **集合框架**:Java集合框架是数据结构和...

    《Java语言程序设计(进阶篇)》 课后习题第21章代码chapter21.rar

    《Java语言程序设计(进阶篇)》是深入学习Java编程的一本重要教材,其中第21章的课后习题代码集包含了丰富的Java高级特性应用实例,旨在帮助读者巩固和提升在面向对象编程、异常处理、多线程、网络编程、IO流等方面...

    2011.06 - Java语言程序设计-进阶篇(原书第8版

    在Java进阶篇中,会详细介绍List、Set、Map等集合类型的使用场景、性能特点及其提供的各种操作方法。 3. 泛型编程:泛型是Java SE 5版本中引入的一个重要特性,它允许在编译时提供类型安全检查,同时避免了类型转换...

    java必备宝典(经典必备)

    7. **反射**:Java反射机制允许在运行时检查类的信息,如类名、方法名、构造器等,并能动态调用方法和创建对象,是Java动态性的重要体现。 8. **JVM(Java虚拟机)**:理解JVM的工作原理,包括类加载机制、内存模型...

Global site tag (gtag.js) - Google Analytics