Java并发(三):共享受限资源

Java并发(三):共享受限资源

一、Java并发共享受限资源的问题
在使用Java并发时,如果两个或者多个线程需要访问共享资源,那么就会出现共享资源竞争的问题,多个线程试图同时访问同一个共享资源
在这里插入图片描述
当线程A与线程B都是对共享资源进行写入数据,那么由于线程调度机制的不确定性,在线程A写入一半数据时可能出现线程B调度,并写入数据,这将导致数据错误。

二、解决共享资源竞争
使用线程时的一个基本问题是:你永远不知道一个线程何时在运行。因此对于并发工作,需要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现竞争的状况。
防止冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前无法访问它,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推。
在这里插入图片描述
基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,这意味着在给定时刻只允许一个任务访问共享资源

  1. 使用synchronized上锁
    Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。如果某个任务处于一个标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronize的方法的线程都会被阻塞
    在这里插入图片描述
    所有对象都自动含有单一的锁,当对象上调用其任意synchronized方法时,此对象被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放锁之后才能被调用。例如上面的代码,某个任务调用了对象的f(),那么Demo对象被上锁,其他线程无法调用f()和g()。
    在这里插入图片描述
    注意,在使用并发时,将域设置为private是非常重要的,否则synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
    在这里插入图片描述
    一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第一个方法,后者又调用了同一对象的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数为0,在任务第一次给对象加锁时,计数为1。每当这个相同的任务在这个对象上获得锁时,计数就会递增。每当任务离开一个synchronized方法,计数递减,当计数为零时,锁被完全释放,此时别的任务才能够使用该资源。
  2. 使用显示的Lock对象
    Lock是显示的互斥机制,其对象必须被显示的创建、锁定和释放,与内建的锁相比代码缺乏优雅性,但是对于一些类型的问题来说更具灵活性。(注意:递增程序是多个步骤的,即在Java中,递增不是原子性的操作。)
    在这里插入图片描述
    如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常,而无法去做任何的清理工作,以维护系统使其处于良好的状态;显示的Lock对象可以在finally子句中将系统维护在正确的状态
    显示的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,赋予了更细致的控制力,其能够对尝试着获取锁等操作。若尝试上锁失败,即可离开该函数去执行其他事情而不是等待锁的释放。
    在这里插入图片描述

三、原子性、可见性与有序性
内存模型的三大特性为:原子性、可见性与有序性。

  1. 原子性
    原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕。但是依赖原子性是很棘手且危险的。
    在这里插入图片描述
    原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当做不可分的操作来操作内存,但是JVM可以将64位(long和double变量)的读取和写入当做两个分离的32位操作,从而导致了可能发生上下文切换。
  2. 可见性
    可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的
    主要有三种实现可见性的方式:
     volatile
     synchronize,对一个变量执行unlock操作之前,必须把变量值同步回主内存
     final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸,那么其他线程就能看到final字段的值
  3. 有序性
    有序性是指在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。也可以通过synchronized来保证有序性。
    在这里插入图片描述
  4. 关于volatile
    值得一提的是,synchronized能够实现原子性和可见性,而volatile实现了内存可见性,但是却无法保证操作的原子性
    volatile实现内存的可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存;而度操作时,会加入一条load指令,即强迫从主内存中读取变量的值。
    被volatile修饰的变量具备了两层语义:
     保证了不同线程对这个变量进行操作时的可见性
    禁止进行指令重排序

四、临界区
有时候希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,那么可以通过synchronized对代码进行分离,该代码段称为临界区。synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。
在这里插入图片描述
这也被称为同步控制块,在进入此段代码之前,必须得到syncObjcet对象的锁,如果其他线程已经得到这个锁,那么就等到锁被释放后才能进入临界区。
通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高

五、在其他对象上同步
前面提到的在方法前面使用synchronized关键字进行修饰,事实上是对方法对应类进行上锁,等同于使用synchronized(this)对类对象进行上锁
在这里插入图片描述
上述代码中,方法f()和time()中的锁的对象是不一致的,f()方法的锁对象是类对象本身,而time()方法锁对象是syncObject,因此可以在不同线程中对同一对象进行同步。
在这里插入图片描述

参考资料:《Java编程思想》
内存可见性和原子性:synchronized和volatile的比较
Java并发编程(三)volatile域

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值