1、进程、线程,多线程
进程:进程是操作系统进行资源分配(包括cpu、内存、磁盘IO等)的最小单位。进程就是程序,有独立的运行内存空间,比如应用和后台服务,比如windows是一个支持多进程的操作系统。内存越大能同时运行的程序越多,在java里一个进程指的是一个独立运行在JVM的程序。如下图,这里就有13个应用进程,以及后台的113个后台进程。
线程:线程是CPU调度和分配的基本单位(可以理解为CPU只能看到线程)。一个程序里运行的多个任务,每个任务就是一个线程,线程是共享内存的。在QQ,微信,钉钉等软件中每个窗口都是一个线程,可以同时接收消息和发送消息,但是只有一个内存占用。如下图的微信界面,每个聊天窗口可以认为是一个线程。
单核多线程: 单核多线程指的是单核CPU轮流执行多个线程,通过给每个线程分配CPU时间片来实现,只是因为这个时间片非常短(几十毫秒),所以在用户角度上感觉是多个线程同时执行。
多核多线程: 多核多线程,可以把多线程分配给不同的核心处理,其他的线程依旧等待,相当于多个线程并行的在执行,而单核多线程只能是并发。
2、并行,并发
并行: 多个CPU实例或是多台机器同时执行一段处理逻辑,是真正的同时。
并发: 一个CPU或一台机器,通过CPU调度算法,让用户看上去同时去执行,实际上从 CPU操作层面并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。
- 单CPU中进程只能是并发,多CPU计算机中进程可以并行。
- 单CPU单核中线程只能并发,单CPU多核中线程可以并行,单核多线程可以并行。
- 无论是并发还是并行,使用者来看,看到的是多进程,多线程。
3、线程的安全性、可见性和原子性
安全性:由于线程之间可以共享内存,则某个对象(变量)是可以被多个线程共享的,是可以被多个线 程同时访问的。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么 就称这个类是线程安全的。
可见性:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。
原子性:原子是世界上的最小单位,具有不可分割性。比如 a=0;这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
举个例子:
甲乙两个人,甲负责向筐里放苹果,乙负责从筐里数苹果,甲乙同时进行,问乙如 何操作才能正确? 不幸的是,以上代码不是线程安全的,因为count++并非是原子操作,实际上,它包含了三个独 立的操作:读取count的值,将值加1,然后将计算结果写入count。如果线程A读到count为10,马上 线程B读到count也为10,线程A加1写入后为11,线程B由于已经读过count值为10,执行加1写入后 依然为11,这样就丢失了一次计数。
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是 一种非常重要的情况,它有一个正式的名字: 竞态条件(Race Condition)。
4、线程运行的内存模型
为什么并发编程会造成上述的不安全性,这就要考虑到我们的线程的内存模型了。
首先我们了解一下计算的硬件架构。
从距离CPU的远近来看,距离最近的是寄存器,然后是缓存,最后是主存(内存)。
CPU只与寄存器中进行存取,寄存的意思是,暂时存放数据,不是每次从内存中取,它就是一个临时放数据的空间,火车站寄存处就是这个意思。而寄存器每次从内存中获取数据。
但是为什么设置缓存呢?因为如果老是操作内存中的同一址地的数据,就会影响速度。于是就在寄存器与内存之间设置一个缓存。当寄存器不断读取同一个内存数据的地址,就会从相应的缓存中读取,而从缓存中读取的速度比从内存读取的速度大的多。
共享对象可见性
两个甚至更多的线程在没有同步的共享对象的时候,一个线程对共享对象的改变对另外一个线程可能是不可见的。为什么造成这样的结果呢?考虑一下,共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中。然后修改了这个对象。只要CPU缓存没有被刷新到主存,共享对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。
下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。
解决这个问题你可以使用Java中的volatile关键字。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去,保证共享对象的可见性,对于另外一个线程来说。
Race Conditions(竞态条件)
考虑到上图的运行的情况:
如果线程A读一个共享对象的变量count到它的CPU缓存中,同时线程B也做了同样的事情。现在线程A将count加1,线程B也做了同样的事情。现在count都是自增为2并且在不同的缓冲中。如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值都是2,并不是期望的3的结果。解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。
5、 java内存模型(JMM)
java 内存模型(java Memory Model)描述了java程序中各种变量(线程共享变量)的访问规则,以及JVM将变量存储到内存以及从内存读取的变量的底层细节。
- 所有的变量都存储在主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本 (主内存中该变量的一份拷贝)
x为共享变量
两个规定
1、线程对共享变量的操作都必须在自己工作内存中进行,不能从主内存中读写。
2、不同线程不能访问其它线程中工作内存的变量,线程间变量间的传递必须通过主内存来完成。
共享变量可见性的实现:
1、线程修改后的共享变量能够及时从工作内存刷新到主内存中
2、其它线程也能及时从主内存把共享变量刷新到自己的工作内存中去
java中语言层面支持的可见性的实现方式:
- synchronized (原子)可见性
- volatile 保证可见性并不是原子性
synchronized
JMM关于synchronized的两条规定
- 线程解锁前,必须把共享变量的值更新到主内存中
- 线程加锁前,必须清空工作内存的变量的值,从而使用共享变量时需从主内存中重新读取最新的值
6、锁
Race Conditions (竟态条件) 会使运行结果变得不可靠,程序的运行结果取决于方法的调用顺序,将方法以串行的方式来访问,我们称这种方式为同步锁(synchronized)。
Java实现同步锁的方式有:
Ø 同步方法synchronized method
Ø 同步代码块 synchronized(Lock)
Ø 等待与唤醒 wait 和 notify
Ø 使用特殊域变量(volatile)实现线程同步
Ø 使用重入锁实现线程同步ReentrantLock
Ø 使用局部变量实现线程同步ThreadLocal
synchronized是Java 的内置锁机制,是在JVM上的 可以同步代码块,也可以同步方法
//同步代码块
synchronized(object){ }
//同步方法
public synchronized void method() { // do something }
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法。
ReentrantLock 可重入锁,是一种显示锁,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同 的基本行为和语义,并且扩展了其能力。
ReentrantLock() :
创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
volatile具有可见性、有序性,不具备原子性。 我们了解到synchronized是阻塞式同步,称为重量级锁。 而volatile是非阻塞式同步,称为轻量级锁。 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值, 从而避免出现数据脏读的现象。