Java内存模型和线程

由于处理器的速度和内存读写的速度差异很大,造成了处理器花很大一部分时间去进行内存读写。因此,计算系统筒加入了高速缓存,以减少处理器等待数据读写的时间。但是, 由于多个处理器对应多个高速缓存,它们共享同一个主存,因此,存在缓存一致性的问题。在主存进行读写时,会遵循缓存一致性协议。此外,处理器会对代码进行乱序执行以充分利用资源,执行的结果会保证和顺序执行的结果一直,即指令重排序优化。

Java内存模型

Java Memory Model, JMM, 它由Java定义,来屏蔽掉各种硬件和操作系统内存访问的差异,让程序能够在不同的平台达到一致性并发的效果

主内存和工作内存

所有的变量都存储在主内存当中,每条线程还有自己的工作内存,类似于高速缓存,线程保存着被该线程使用到的变量的拷贝,线程对变量的操作在工作内存中,不直接写主存。线程操作其他线程的工作内存。

内存间的交互操作

主内存和工作内存之间的数据同步需要一些操作来完成,Java内存模型定义了几种操作来完成:

  • lock(锁定):作用于主存,把主存的变量标记某个线程独占

  • unlock(解锁):作用于主存,把其从锁定状态释放出来

  • read(读取):主存读取到工作内存

  • load(载入):作用于工作内存,把read操作得到的值放入到工作内存中。

  • use(使用):作用于工作内存,把变量传递给执行器引擎

  • assign(赋值):作用于工作内存,把执行引擎得到的值赋值给工作内存变量

  • store(存储):作用于工作内存,把工作内存中的一个变量值传送给主内存中,为write操作

  • write(写入):作用于主内存,写入主存

由上面分析,read和load必须同时出现,store和write必须同时出现,但是不保证一定顺序执行,有可能read和load之间有其他操作。

volatile型变量

轻量级同步机制,它具备两种基本特性:

  • 保证此变量对所有线程的可见性,即当一条线程修改了该变量,新的变量对其他线程立刻可见。这里的可见性是有些歧义,实际上,使用volatile关键字,修改时立刻写回主存,而使用时立刻刷新(从主存读一次),因此,只有在它使用时才会确保一致。但是通过上诉的内存间交互操作可知,从主存读取到工作内存的操作并不是原子的,实际上分为两步,read和load,如果在read后有其他线程立刻修改,则会造成并发下数据不一致的情况。那什么情况下才能使用volatile呢?1)运算结果不依赖变量的当前值,或者只有单一线程修改该变量。如i++的结果依赖于i,而i=4则不依赖于i; 2)变量不需要于其他的状态变量共同参与不变约束

  • 提供内存屏障,禁止指令重排序优化。普通变量仅会保证在方法执行的过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序和代码的顺序是一致的。我的理解是:

    1
    2
    i++;
    j++;

可能由于j正好在寄存器中,所以先执行了j++,在执行i++,这里j和i相互不依赖。

大多数情况下,volatile比锁性能要好,但是很难量化,因为锁存在很多优化的手段。而对于volatile关键字,读操作和普通变量相差不大,写操作则会相对慢些。

特殊的long和double

由于long和double在内存中占用两个slot,因此读写分两次操作,所以,不保证read,load,assign和write的原则性(其他基本类型保证)。使用volatile可保证,但是出错的情况非常少见,而且商用的虚拟机都选择64位读写来作为原子操作,因此不必特意申明其为volatile。

线程

线程的引进,是将资源的分配和执行调度分开,各个线程共享进程资源(内存地址,文件I/O等),同时线程能够独立调度,它是CPU掉的的基本单位

线程的实现

主流的操作系统都实现了线程的实现,而Java则提供了在不同平台,不同系统的统一实现,每个线程都是java.lang.Thread的实例,这个类比较特殊,因为它的所关键方法,都是Native方法,这样的方法表示该方法没有使用或无法使用平台无关的手段来实现。

实现线程主要有三种方式:内核线程,用户线程和用户线程加轻量级进程混合。这一段有些深入,目前不太了解

状态转换

Java定义了线程的5中状态,在任意的一个时间点,线程只能处于其中的一种状态。

  • 新建(New)线程对象以创建,Thread对象存在,但是没有启动,未调用start()方法

  • 运行(Runable)这个状态包括了操作系统中的Running和Ready,处在这个状态,它可能正在执行(占用CPU时间),也可能正在等待CPU分配执行时间。

  • 无线等待(Waiting)CPU不会分配执行时间给该线程,它要等到一些外部条件(notify)触发才能进入运行状态,页就是说,它需要被其他线程显示唤醒。可以使用下面几种方式使线程进入waiting状态。1) Object.wait() 2) Thread.join() 3) LockSupport.park()

  • 期限等待(Timed Waitting)处于改状态的线程也不会被分配CPU时间,不过它无需被其他线程显示唤醒,而是在一段时间后有系统自动唤醒,以下方法能使线程进入该状态。1) Thread.sleep() 2) 设置了timeout的wait()和join()

  • 阻塞(Blocked)它线程试图获取一个排它锁而未遂时,进入该状态,最常见的就是sychronized尝试进入同步区域时。

  • 结束(Terminated)线程已经终止,执行完毕。

线程相关的方法

  • sleep() 它是Thread的static方法,使得线程从Runable态进入Timed Waiting态,因此它和锁不相关,它可以在任何地方使用,不必再sychronized中就能使用,如果在sychronized块中使用,它也不会释放锁。

  • wait() 它是Object类的成员方法,因此,每个对象均有该方法,它可以使线程从runable态进入waiting态(不加时间)或进入timed waiting态(加时间),由于它是对象方法,因此调用它时必须持有该对象的锁,也即要在sychronized块中使用,否则在运行时抛出java.lang.IllegalMonitorStateException,调用后,它会释放锁

  • notify() 它是Object类的成员方法, 也是每个对象的都有,由此可知必须拥有该对象的锁才能调用该方法。它可以通知一个在该对象等待池冲的线程,从waiting状态到runable状态,但是该线程并不会马上执行,会等待CPU分配执行时间,即ready状态

  • notifyAll() 同上,只是不是随机通知一个线程,而是通知所有线程,从waiting 到 runable,然后他们相互竞争CPU时间运行。

  • yield() 和sleep()一样,是Thread的静态方法,它可以让自己立刻让出CPU的执行时间,让CPU重新分配时间给其他线程执行。它的状态没改变,从runable-> runable,如果非要说改变了,可以说是从running -> ready

  • join() 它是线程的实例方法,可以使得一个线程在另一个线程结束后再执行。使得该线程从runable到waiting(不加时间)或timed waiting(加时间)。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行,最简单的例子就是主线程Main中,调用子线程t的t.join(),表示Main线程会等到t线程结束后在执行。也可在join中传入时间,表示过了一段时候后,Main不在等待,重新竞争CPU时间。

  • interrupt() 它是线程的实例方法,调用处于waiting和timed waiting状态的线程时,该线程的中断状态将被清除,会抛出InterruptedException。