JAVA内存模型、volatile、线程安全、锁详细解说

一. JAVA内存模型

首先,我们要清楚,这里所指的java内存模型java内存区域 不是一个层次的划分。java内存模型:工作内存和主内存
java内存模型图
1、内存模型的目的:
这个内存模型的划分是为了清晰地了解多线程之间,是如何共享数据,独占数据的。
每个线程都是在自己独立的工作内存中进行数据操作的,且数据处理完成后都要入主内存。所以,不难看出,多线程的共享数据是通过主内存来同步的。

2、内存模型中,变量数据是如何交互的:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出啊俩,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后续使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本。
use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量复制的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存变量中。
java内存模型对这8种基本操作有一些基本规则,在此不展开。

3、volatile关键字如何做到让变量对所有线程保持可见性
volatile关键字能保证变量的可见性,即当某个线程修改了该变量值时,其他线程可以立即知道。但是并不是任何场景都可以用volatile进行防并发处理。只有符合以下2个条件的情况下,才可以用volatile防并发。
(1)运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改变量的值;(2)变量不需要与其他的状态变量共同参与不变约束。

volatile关键字可以防止指令重排序,指令重排序是机器级的优化操作,简单可以理解为一个线程观察另一个线程的时候,所有操作都是无序的,即和代码的顺序不一定相同,前提都会得到正确结果;但是本线程中观察,所有错操作都是有序的。
阅读到这里,再声明一下,多线程的工作内存是线程独占的,而主内存是共享的。volatile关键字修饰的变量,其实还是基于上述8中基本操作而实现的,区别在于,
volatile对于8个原子性的操作有额外的规则:
read、load、use这3个操作必须连续出现,相互依赖。这保证了volatile修饰的变量值每次线程调用时都是从主内存中获取的。
assign、store、write这3个操作必须连续出现,相互依赖。这保证了volatile修饰的变量值每次被线程修改都要入主内存。

模型的特征:
原子性:操作原子
可见性:volatile修饰的变量
有序性:volatile防止指令重排序,synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,持有同一个锁的两个同步块只能串行进行。

4、先行发生原则
我们不能只依赖于volatile、sychronized关键字来帮助解决并发编程。先行发生是用来判断数据是否存在竞争、线程是否安全的主要依据。
满足先行发生原则,我们就无需任何同步协助。
java内存模型中的先行发生规则:
(1)程序次序规则:即在一个线程中,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支循环等。
(2)管程锁定原则:一个unlock操作先行发生于后面对同一个锁的lock操作,这里必须强调是同一个锁,而”后面”是指时间上的先后顺序。
(3)volatile变量原则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,”后面”是指时间上的先后顺序。
(4)线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作。
(5)线程终止原则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
(6)线程终止原则
(7)对象终结原则
(8)传递性

二、 线程安全和锁

1、线程的实现
内核线程实现,用户线程实现,用户线程加轻量级进程混合实现。在java中,sunjdk是将一条java线程映射到一条轻量级进程中,每个轻量级进程都有一个内核线程支持,这通过内核线程的一种高级接口实现。轻量级进程和内核线程一一对应。广义上说,除了内核线程,其他的线程全是用户线程,包括轻量级进程,但是轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制。用户线程的所有操作不需要内核的帮助,所以非常快,但是实现起来比较复杂,现在使用用户线程的程序越来越少。
线程的调度又分为协同式调度和抢占式调度。 协同式调度的多线程系统,线程的执行时间是由线程本身来控制,线程工作执行完了,主动通知系统切换到另外一个线程上,这不会涉及线程同步的问题,但是相当的不稳定,如果某个线程一直占用CPU执行时间就可能导致整个系统崩溃。而java采用的是抢占式调度,每个线程将有系统来分配时间,线程只能决定让出时间而不能主动获取时间。线程优先级的设置可以让线程的时间调度更加优选,但实际不太靠谱。具体原因不在讲述。

2、java线程的5种状态
New:创建后尚未启动的线程处于这种状态。
Runnable:处于运行中或等待CPU分配时间的状态。
Waiting:处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。
Timed Waiting:处于这种状态的线程也不会被分配CPU执行时间,无需等待被其他线程显式地唤醒,在一定时间后会由系统字段唤醒。
Blocked:线程被阻塞,等待获取到一个排他锁。
Terminated:线程已经终止执行。

3. 线程安全
线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何协调操作,调用对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
安全的分阶:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。java中绝大多数所谓的线程安全指的是相对线程安全。我们这里注意一下相对线程安全,这是指对象的单独操作是安全的,但是特定顺序的联系操作就需要进行额外的保障了。比如:在java中,vector是一个线程安全的容器,但是当2个线程,一个线程A对vector对象进行remove(i),另一个线程对vector对象进行get(i)可能会获取到0。这里发生了线程并发问题。线程兼容指的是,对象本身并不安全,但可以通过同步手段保证并发环境下的安全使用。
安全的实现方式互斥同步,诸如sychronized同步锁、ReentrantLock重入锁。做1.6之前ReentrantLock的性能更为出色,但是未来的趋势还是原生的sychronized。
非阻塞同步,CVS,基于冲突检测和乐观并发策略。通俗的说就是先进行操作,如果没有其他线程争用共享数据,那就操作成功了,否则采取补偿措施。无同步,这包括可重入代码、线程本地存储(ThreadLocal)。

4. java锁的优化
自适应性自旋:这是解决,互斥同步时,阻塞情况,线程挂起的时候,线程从用户态和内核态之间的转换消耗性能。这个优化,使阻塞的线程更加聪明的选择是等待获取 锁还是挂起。
锁消除:虚拟机会自动观察被锁的对象的动态作用域,如果在方法内部,对象永远不会逃逸,那么在编译期间会忽略调所有的同步直接执行。
锁粗化:虚拟机会自动探测频繁进行加锁解锁操作,合理扩大锁的范围来减少这些消耗性能的操作。
轻量级锁、偏向锁,这2个锁要理解需涉及到虚拟机的布局,笔者还没有阅读那个部分。等有机会再和大家共享。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值