物理计算机中的并发问题:大多数的计算任务不能只由处理器完成,而需要通过处理器与内存交互,而处理器处理的速度与内存读取的速度之间差了几个数据集,因此引入了高速缓存来作为内存与处理器之间的缓冲。
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中。
但这样会涉及到一个重要问题:如果多个处理器处理的是同一块内存,就可能导致缓存不一致,进而影响在主内存的存储。
除了高速缓存外,还引入了乱序优化。为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,最后再进行重组。Java中也有对指令的重排序优化。
Java内存模型的主要目标是定义程序中各个变量的访问规则。这里的变量不包括局部变量与方法参数,因为它们是被线程所私有的,不涉及共用。这里的变量是指对象实例,静态变量,和构成数组对象的元素。
java的内存模型包括主内存与工作内存,主内存用于存储所有的变量,而工作内存是每个线程独有的,工作内存中储存了该线程所用到的变量的主内存副本拷贝(不会拷贝整个对象,而是拷贝对象中的部分字段),一个线程对变量进行操作(读取或更改)都需要对工作内存进行操作,而不是直接向主内存操作。
基本规则:
1.保证变量对所有线程都是可见的,当一个线程更改了这个变量的值,那么当前变量的值对于其他线程是立刻可见的。普通变量是无法做到的,因为需要先在工作内存中赋值(assign),然后在发送回主内存修改。(store->write)
2.禁止指令重排序。指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处,但要求重排序不会影响结果。例如指令一要求地址a中的值+10,指令二要求地址a中的值*2,而指令三要求地址b中的值+10。这样的话我们可以将指令三放到指令一和指令二之间,因为指令三和指令一指令三之间没有关系,但指令一不能在指令二后面因为(a+10)*2
和(a*2)+10
的结果不同。
而volatile的思想是设置了内存屏障,不允许将内存屏障后面的指令排序到内存屏障之前,只有一个线程时不需要内存屏障,多个线程才会需要,因为单一线程的字节码是串行执行的。
原子性:原子性就是指对数据的操作是一个独立的、不可分割的整体。也就是说当数据进行一个操作时,不要切换线程,要在执行结束后才可以切换线程。
以图中为例,我们要想执行i++这个操作, 首先就要i=i+1,之后存储i=2,不能再这个过程中切换到其他线程。
一般解决互斥性和原子性的方式是Synchronized方法,或Lock方法。
Synchronized方法通过monitorenter和monitorexit来隐式的操作lock和unlock操作。
可见性是指当一个线程更改了变量的值后,其他线程能立刻得知该变量的值更改。java内存中是通过变量修改后再同步到主内存,在变量读取前从主内存中刷新这种依赖主内存作为媒介的方式来保证的可见性。
除volatile外,java中还有final和synchronized可以实现可见性。
同步块的可见性是当我们assign赋值后,必须先store并write进主内存后才可以调用unlock方法。
而被final修饰的变量,只要在初始化结束后,且没有this赋值逃逸,就可以被所有线程可见。
线程内字节码以串行执行,线程外禁止指令重排序以保证有序性。
volatile是使用内存屏障组织指令重排序,而synchronized是同一时刻只允许一个线程对对象进行lock操作。
以单例模式为例,new Singleton()其实是三步:(1)为对象分配内存;(2)执行构造函数,初始化成员变量(3)将对象指向分配的内存(此时instance就不是null了),但jvm中允许乱序执行,有可能出现(1)(3)(2)的顺序,那么假如A线程执行了(1)(3),而此时B线程调用getInstance就会返回一个instance对象,但调用上就会出错。
Java 中也可通过Synchronized或Volatile来保证顺序性。
内核线程(KLT)就是直接由系统内核操作的线程,这种线程由内核完成切换,通过调度器(Scheduler)调度对线程进行调度,并负责将线程的任务分配给各个CPU去执行。
而实际中我们不会直接使用内核线程,而是使用轻量级进程(LWP),一个轻量级进程要对应一个内核线程。轻量级进程与内核线程是1:1的对应关系。
优点:每个轻量级进程都是一个独立的调度单元,即使一个进程被堵塞了,也不影响其他进程正常工作。
缺点:需要在内核态与用户态来回切换。同时每个轻量级进程需要内核线程的支持,因此一个系统所能有的轻量级线程是有限的。
指完全建立在用户的线程库上,而系统内核不能感知线程存在的实现。
优点:由于用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助,甚至可以不需要切换到内核态,所以操作非常快速且低消耗的,且可以支持规模更大的线程数量。
缺点:由于没有系统内核的支援,所有的线程操作都需要用户程序自己处理,线程的创建、切换和调度都是需要考虑的问题,实现较复杂。
一对多的线程模型进程:进程与用户线程之间1:N的关系,如图所示
协同式线程调度:线程的执行时间由自己决定,当线程执行结束后会主动通知系统切换线程。只有当前执行线程执行结束后才会继续执行下一个线程。这样做的后果是可能由于错误操作导致其他线程一直处于阻塞状态。
抢占式线程调度:每个线程的执行时间由系统决定,线程执行时间是系统可控的,不存在一个线程导致整个进程阻塞的问题。可以通过设置线程优先级,优先级越高的线程越容易被系统选择执行。
Object.wait()
Thread.join()
Thread.sleep()
Object.wait()
Thread.join()
wait方法的作用就是阻塞当前线程等待notify/notifyAll方法的唤醒,或等待超时后自动唤醒。调用wait方法后,线程是会释放对monitor对象的所有权的。
既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而notifyAll则唤醒所有的线程。
这个结果的区别很明显,通过sleep方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的,而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。
Synchronized的使用原理:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
monitorenter:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是object所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
对于普通同步方法,锁是当前实例对象。因此在类Test中对两个方法进行上锁,同时在另一个类中创建一个Test实例,并创建两个线程分别执行Test实例的A、B两个方法,两个线程会按顺序执行。线程2需要等待线程1执行完才能执行。
**对于静态同步方法,锁是当前类的Class对象。**当我们在类Test中创建了两个静态方法,同时对静态方法上锁。在另一个类中即使创建两个对象,并创建两个线程分别执行对象1的A方法和对象2的B方法,线程2也会等线程1结束之后才能执行。
对于同步方法块,锁是Synchonized括号里配置的对象。对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。