并发(3)共享受限资源

如果世界只有一个人,那么这个人可以操作任何的东西,不会有另外的人跟他抢夺东西;而真实的世界是存在很多人,他们可以同时做同样的事情,就有可能互相干扰,多线程程序也是如此。所以无论是现实世界还是虚拟的程序中,都要防止这种冲突。

21.3.1 不正确的访问资源

多线程造成的问题在于同时可以操作一个对象,导致对象被一个线程变更后,另外一个线程又去变更,此时第一个线程还按照之前的值进行计算,就会出现问题。

Java中的自增运算法++不是原子性的,线程不安全的。

比如:

int value = 0;

public int next() {

++value;

++value;

}

这种情况下,多线程执行的结果不一定是2的倍数。

21.3.2解决共享资源竞争

如果现实世界是一种程序的仿真,出现线程不安全的场景会让你莫名其妙。比如你刚坐上汽车,车就瞬间开出10公里外,是不是很不可思议。还好真实的世界没有上帝之手在操控。

现实世界中,当这个汽车是你的时候,其他人在合法的情况下是不能占用的。这意味着在给定时刻只允许一个任务访问资源。而你拥有汽车的凭证可以认为是一种互斥量(mutext).举一个公共资源的例子,比如卫生间有个小门,当有一个人进去的时候,会把门锁上,其他人看到之后就会在外面等,直到第一个人出来,后面的人才能进去;这个门锁可以认为是一种互斥量。程序中,当锁被打开后,等待的线程会随机的获取到锁;你也可以通过setPriority()给线程调度器建议,实际效果要看具体平台以及JVM的实现。

Java提供关键字synchronized,为防止资源冲突提供了内置支持。

共享资源一般是以对象形式存在的内存片段,也可以是文件、IO端口。要控制共享资源的访问,把访问共享资源的方法通过synchronized标记。

所有的对象都有单一的锁(监视器),当访问对象的方法被synchronized标记后,调用方法会在对象上加锁,调用完毕后释放锁。使用并发时,需要将域设置成private,避免非synchronized方法访问到对象,否则产生并发问题。

一个任务可以多次获取对象锁。比如访问了同步方法1,又访问同步方法2,此时均包含同一个对象,那么对象锁的计数器会变成2,执行完一个方法,锁计数器减1.

什么时候使用同步?当你的对象可能同时被多个任务使用时,读写都需要使用相同的锁同步。

使用Lock对象,可以显式的使用互斥机制。

创建锁对象:Lock lock = new ReentrantLock();

代码加锁:lock.lock();

代码关闭锁:lock.unlock();

一般lock()和unlock()之间的代码使用异常捕获机制try,代码执行完后,在finally代码快中执行unlock(),确保unlock一定会被执行。

synchronized与Lock的区别:

使用synchronized,代码量更少,编码出错的概率低,优先使用。

Lock主要用于一些特殊场景,比如获取锁设置超时等待时间。lock.tryLock()尝试获取锁,lock.tryLock(Integer, TimeType)尝试获取锁一段时间,超时后放弃。

21.3.3原子性与易变性

一个不正确的认识“原子操作不需要进行同步控制”。原子操作,是指不能被线程调度机制中断的操作,一旦操作开始,一定可以在可能发生的上下文切换之前执行完毕,不可再分解的操作。

long和double在64位的JVM上的读取和写入会分离成两个32位的操作,所以它的操作可能被上下文切换中断,产生不正确的结果。可以使用volatile关键字,通过线程机制保证其不可中断。

在多处理器系统,可视性问题很多。不同任务对应不同的视图(视图可以理解成线程自己的本地变量),任务之间视图是不可见的。同步机制强制在处理器的系统中,一个任务做出的修改必须在应用中是可视的。操作系统提供这样的机制,volitile则可以利用这个机制。

volatile确保了应用的可视性。一个被声明为volatile的域,只要产生写操作,会立即被写入到主存中;其他任务读取的时候都会从主存中读取。非valatile的域则不会从主存中读取属性值。

如果一个域的改变完全被synchronized修饰的方法或语句块保护,那么不必须设置volatile。

21.3.4原子类

Java提供原子性变量类,AtomicInteger、AtomicLong、AtomicReference等。他们提供以下形式的原子性条件更新操作:

boolean compareAndSet(expectedValue, updateValue);

常规的编程中不需要使用原子类,多线程编程才会用到。用锁来保持同步要比原子类更安全一些。

21.3.5临界区

临界区,也叫同步控制块,将方法内部的部分代码进行同步,而不是防止访问整个方法,这些部分代码被称为临界区。代码示例:

synchronized (syncObject) {

//这些代码可以同时被一个线程访问

}

进入此代码前,必须先获取syncObject对象的锁。如果其他线程获取了该锁,就得等锁释放才能进入执行这部分临界区的代码。

优势:通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。

我们知道,同步本身会控制线程对于资源的访问。如果在一个执行方法中,由于内部逻辑执行较多,整体执行时间较长,此刻对整个方法进行同步控制,所有的多线程访问到了这里就会编程顺序执行,性能之差逐步变为程序的瓶颈。应该分析的是,在这个大而性能差的方法中,哪些逻辑是不需要进行同步的,我们只需对有共享资源访问的模块进行同步控制即可,除了将方法拆分细化,临界区给我们提供了一种可能,即在一个方法内,对同步进行更细粒度的控制。

21.3.6在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,synchronized(this),临界区的效果仅仅局限在访问当前对象的多线程任务上。

这涉及到同步锁生效的范围,this只是当前对象的锁范围;

如果多个流程中需要同样的锁进行同步,那么则需要指定一个其他对象;

如果要在当前jvm程序中进行全局同步,则需要一个全局的对象,用来提供对象锁,可以是一个静态对象;

如果要在多个jvm程序中进行全局同步,那么就需要一个超脱于jvm的一个对象标识,我们一般通过缓存中间件来处理,就是我们常说的分布式锁机制。

对象锁是多线程更底层的实现机制,会在其他文章中介绍。

21.3.7线程本地存储

使用线程同步是为了防止一个对象被多个线程同时访问,造成读取的信息有差异,进而导致程序不符合预期。而在某些场景下,则需要线程保持各自的特征,方便进行标识。

创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现。ThreadLocal是泛型包装类,可以放其他类型的对象。

ThreadLocal对象通常当做静态域存储。在创建ThreadLocal时,只能通过set()、get()访问该对象的内容。get()方法将返回与其线程相关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。get()方法不是同步的,因为ThreadLocal保证不会出现竞争条件。

点击去京东购买《Java编程思想(第四版)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值