Java内存模型与线程

硬件效率与一致性

由于存储设备和处理器运算速度之间的存在巨大的差异,现在计算机系统在内存与处理器之间加入高速缓存来作为处理器与内存之间的缓冲。将处理器需要的数据复制到缓存中,让处理器可以快速的获取数据进行计算,计算结束后再从缓存同步带内存中去,这样处理器无需等待缓慢的内存读写。

如此看似美好,但引入了一个新的问题:缓存的一致性。在一个多处理器系统中,每个处理器有自己的高速缓存,它们共享同一主存,这样在运算的时候会出现多个处理器的缓存不一致问题。对于这个问题需要用到缓存一致协议。

处理器、高速缓存、主内存之间交互关系如下:

 

线程、主内存、工作内存交互关系

 

每条Java线程有自己的工作内存(相当于一个高速缓存),工作内存中保存了线程需要的变量的主内存副本拷贝。线程对变量的操作必须在工作内存中进行而不是直接写入主内存中的变量。不同线程间也无法直接直接访问对方工作内存,线程间变量值传递还是需要主内存。

 

内存间的8种类操作

工作内存和主内存之间的交互协议,即如何将一个变量从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型使用了8种原子操作来完成。

 

                              Wirte(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量。

volatile型变量的特殊规则

Volotile关键字是Java虚拟机提供的最轻量级的同步机制。当一个变量被volatile修饰后,它有两个特性:

1)保证变量对所有线程的可见性(但是不保证原子性,所以多线访问volatile修饰的变量不能保证线程安全)。使用volatile修饰的变量被一个线程(CPU)修改后会立即将修改后的值写入主内存,并把其他线程(CPU)中的缓存无效化(缓存有效位置为无效)。这样一个操作可用让一个线程(CPU)对volatile变量的修改对于其他线程(CPU)立即可见。

2)禁止指令重排序优化指令的重排序在单个线程内是不会影响程序执行的正确性。但是在多线程环境下,指令的重排序可会使程序产生不确定的结果。而禁止指令重排序使用一个lock操作,这个lock操作相当于一个内存屏障,保证指令的相对有序性。

Synchronized和Lock也能保证线程的可见性。

原子性、可见性、有序性

Java内存模型是围绕并发过程中如何处理原子性、可见性、有序性3个特诊来建立的。

1)原子性:在Java中,对基本数据类型的访问读写都是原子性的(long、double除外)。

Int y = 1是一个原子操作,x++不是原子操作(3个操作:先读取x,x加1,加1后的新值写入x)。Java内存模型提供了lock和unlock这两个操作来满足原子操作需求。在字节码层次是使用monitorenter和monitorexit指令隐式使用这两个操作,在Java代码层次就是同步块synchronize。所以synchronize块之间的操作具有原子性。

2)可见性:可见性是指一个线程修改了共享变量的值,其他线程可以立即得到这个修改。Java中synchronize和final关键字也可以实现可见性。

3)有序性:可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

看起来synchronized是一个万能的并发控制关键字,但是它的使用也伴随着大的性能影响。

线程的实现

线程是比进程更轻量级的调度执行单位。线程引入,把进程的资源分配和执行调度分开。各个线程共享进程资源(内存地址、文件IO等),有可以独立调度。

实现的实现方式:

1)使用内核线程实现

内核线程(KLT)是操作系统直接支持的线程。程序一般不直接使用内核线程,而是使用内核线程的一种高级接口:轻量级进程(Light Weight Process)。轻量级进程就是通常意义上所将的线程。每个轻量级进程LWP都需要一个内核线程的支持。

 

操作系统内核通过操纵调度器(Scheduler)对线程进行调度,将线程的任务映射到各个处理器上。每个内核线程可以作为内核的一个分身。上图中的P表示一个进程(Process),LWP是轻量级进程,KLT是内核线程。可以看到一个LWP需要一个KLT的支持。这么做的好处是一个LWP阻塞了不影响整个进程的工作。缺点是线程的创建、阻塞、唤醒操作需要进行系统调用,从用户态切入内核态,开销较大,并且一个LWP需要一个KLT,而KTL是需要消耗内核资源。 

2)使用用户线程实现

用户线程是指完全建立在用户空间上的线程库上。线程的创建、同步、销毁和调度都在用户态中完成,所以都是非常快速和低消耗的。这个线程实现方式中进程与用户线程的关系是1:N的。

 

用户线程的优点是不需要内核支援,所以他说快速低消耗的。缺点就是用户程序需要去处理线程的各种操作如创建、同步、调度等。实现复杂,现在Java都放弃这个方式了。

3)使用用户线程加轻量级进程混合实现

线程除了依赖内核线程和完全由用户程序自己实现外,还有一种将内核线程与用户线程结合的实现方式。实现方式如下图所示:

 

上面UT就是用户线程,一个轻量级进程LWP可以支持一个或多个UT,每个LWP又是有一个KTL支持的。这种实现方式的特点有:

用户线程的操作依旧廉价,并可以支持大规模用户线程并发。

LWP作为用户线程和内核线程的桥梁,可以使内核线程提供线程调度功能和处理器映射。

用户线程的系统调用需要通过LWP完成,可以降低整个进程被阻塞的风险。

这种实现方式用户线程和轻量级进程的关系是N:M。

Java线程调度

线程调度是系统为线程分配处理使用权的过程。

线程调度的两种方式:

1)协同式线程调度线程的执行时间由线程本身来控制,一个线程执行完后主动通知线程切换到另一个线程。这种方式的优点是实现简单,线程是顺序执行的,不需要处理同步问题。缺点是线程执行时间不可控,如果一个线程出现问题,一直不进行线程切换,会导致程序阻塞。

2)抢占式线程调度:由系统来分配每个线程的执行时间,线程的切换不由线程本身决定,而由系统来决定,不会出现一个线程导致整个进程阻塞。

线程状态转换

Java语言定义了5种线程的状态,在任意一个时间点,一个线程有且只有一种状态。

新建New):线程被创建但是未启动。

运行Runable):包括Running和Ready,处于此状态的线程可能在运行或者是Ready状态等待CPU时间的分配。

无期限等待Waiting):处于此状态的线程不会被分配CPU时间,需要等待其他线程显示唤醒。下面几种方式可以时线程进入无期限等待状态:

没有设置参数的Object.wait()方法。

没有设置参数的Thread.join()方法。

LockSupport.park()方法

有期限等待Timed Waiting):处于这种状态的线程不会被分配CPU时间,但是不需要等待被其他线程唤醒,系统在一定时间后会唤醒这种状态的线程。以下几种方法可以使线程进入有期限等待状态:

(1)Thread.sleep()方法

(2)设置了参数的Object.wait()方法。

(3)没有设置了参数的Thread.join()方法。

(1)LockSupport.parkNaos()方法

(2)LockSupport.parkUntil()方法

阻塞状态Blocked):与等待状态的区别是,阻塞状态等待获取到一个排他锁,线程在等待进入同步区域的时候处于这种状态。

结束:已经终止的线程处于的状态。

线程状态转换图:

 

阻塞与等待共同点和区别?

共同点:都不会被分配CPU时间。

区别:阻塞状态是线程等待一个排他锁,而这个锁被其他线程占用进入的状态。等待状态是一个线程等待另一个线程通知调度器一个条件的时候进入的状态。

思考

Sleep()和wait()方法的区别?

1)、Sleep()是Thread类的方法,wait是Object类的方法

2)、Sleep()方法是让线程暂停执行,线程的监控状态仍然保持,暂停时间结束,线程马上自动恢复运行状态。Sleep()方法不会释放锁对象。Wait()方法会放弃对象锁,进入等待池等待,线程等待针对此对象的notify/notifyAll方法才可以被唤醒


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值