很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
并发与并行
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
Java内存模型的抽象示意图如上图;
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
线程间通信的步骤:
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
然后,线程B到主内存中去读取线程A之前已更新过的共享变量。如下图:
解释下上文所说的主内存和本地内存:
首先,JVM将内存组织为主内存和工作内存两个部分。
主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。
2.每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
3.线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。见下图:
正常的JVM模型如下:
省略了本地方法栈(本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。),上图所提到的栈都属于虚拟机栈。
说到多线程就不能不提到线程安全这个概念:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
由于线程访问无状态对象的行为并不会影响其它线程中操作的正确性,因此无状态对象是线程安全的。大多数Servlet都是无状态的,从而极大地降低了在实现Servlet线程安全性时的复杂性。只有当Servlet在处理请求时需要保存一些信息,线程安全才会成为一个问题。
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。对象的状态是指存储在状态变量(例如实例或静态域)中的数据,对象的状态可能包括其他依赖对象的域。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。访问某个变量的代码越少,就越容易确保对变量的所有访问都实现正确同步,同时也更容易找出变量在哪些条件下被访问,Java语言并没有强制要求将状态都封装在类中,开发人员完全可以将状态保存在某个公开的域(甚至公开的静态域)中,或者提供一个对内部对象的公开引用。然而,程序状态的封装性越好,就越容易实现程序的线程安全性,并且代码的维护人员也越容易保持这种方式。当设计线程安全类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。
同时保证了线程的原子性和可见性即可保证线程的安全性。
原子性
就像数据库里的定义一样,原子性就是一个操作(可能是需要多步完成的复合操作)不能被打断,一旦开始执行直到执行完其他线程或多核都必须等待。比如”i++”表达式,就不是原子的,汇编后会发现由三条指令(读取,修改,写入)完成,每一条指令完成后都可能被中断。说到原子性,一般会提到可见性,这两者其实没有任何联系,但这两个因素确是同时影响到多线程安全的特性。只具备原子性或可见性并不能保证线程安全(注意synchronized同时保证了原子性和可见性,只保证原子性可能结果并没有同步到主存,其他线程不可见)。可见性跟jvm的内存结构有关系,前面给出了jvm内存结构图,各个线程或多核对同一个变量有备份(在线程的工作内存中或核的寄存器中,为了节省IO通信等),导致跟jvm主存中的变量值不一致。这样做的目的是为了提高性能。当然在多线程中就可能造成问题,就要用同步来解决了。
可见性
保证内存可见性就是希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。在一个单线程程序中,如果首先改变一个变量的值,再读取该变量的值的时候,所读取到的值就是上次写操作写入的值。也就是说前面操作的结果对后面的操作是肯定可见的。但是在多线程程序中,如果不使用一定的同步机制,就不能保证一个线程所写入的值对另外一个线程是可见的。造成这种情况的原因可能有下面几个:
CPU 内部的缓存:现在的CPU一般都拥有层次结构的几级缓存。CPU直接操作的是缓存中的数据,并在需要的时候把缓存中的数据与主存进行同步。因此在某些时刻,缓存中的数据与主存内的数据可能是不一致的。某个线程所执行的写入操作的新值可能当前还保存在CPU的缓存中,还没有被写回到主存中。这个时候,另外一个线程的读取操作读取的就还是主存中的旧值。
CPU的指令执行顺序:在某些时候,CPU可能改变指令的执行顺序。这有可能导致一个线程过早的看到另外一个线程的写入操作完成之后的新值。
编译器代码重排:出于性能优化的目的,编译器可能在编译的时候对生成的目标代码进行重新排列。
事实上,在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。
Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。volatile保证新值能立即同步到主内存,每次使用前立即从主内存刷新。volatile,sychronized和final都可以实现可见性。synchronized可见性是通过对一个变量执行unlock操作之前,必须把变量同步回主内存实现的。final关键字是非常重要而事实上却经常被忽视其作为同步的作用。本质上讲,final能够做出如下保证:当你创建一个对象时,使用final关键字能够使得另一个线程不会访问到处于“部分创建”的对象,否则是会可能发生的。这是 因为,当用作对象的一个属性时,final有着如下的语义:当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的。使用final是所谓的安全发布的一种方式,在一个线程中创建它,同时另一个线程在之后的某时刻可以引用到该新创建的对象。当JVM调用对象的构造函数时,它必须将各成员赋值,同时存储一个指向该对象的指针。就像其他任何的数据写入一样,这可能是乱序的,就被写入到主存并被访问到了。这样会导致另一个线程看到了一个不合法或不完整的对象。而final可以防止此类事情的发生:如果某个成员是final的,JVM规范做出如下明确的保证:一旦对象引用对其他线程可见,则其final成员也必须正确的赋值了。final的对象引用,对象的final成员的值在当退出构造函数时,他们也是最新的。这意味着:final类型的成员变量的值,包括那些用final引用指向的collections的对象,是读线程安全而无需使用synchronization的