资讯专栏INFORMATION COLUMN

Java并发编程笔记(二)

NickZhou / 2617人阅读

摘要:本文探讨并发中的其它问题线程安全可见性活跃性等等。当闭锁到达结束状态时,门打开并允许所有线程通过。在从返回时被叫醒时,线程被放入锁池,与其他线程竞争重新获得锁。

本文探讨Java并发中的其它问题:线程安全、可见性、活跃性等等。

在行文之前,我想先推荐以下两份资料,质量很高:
极客学院-Java并发编程
读书笔记-《Java并发编程实战》

线程安全
《Java并发编程实战》中提到了太多的术语,比如各种XX性。而安全性我觉得这个概念并不妥。计算机术语中的线程安全大家一说就懂,但老是生造概念就不好了。又例如,活跃性,就是避免饥饿和死锁呗!

线程安全问题就是多线程时结果受执行顺序影响,要解决就要让相关操作具有原子性。这个上过操作系统原理的肯定都知道。至于原子性,不再解释。

那么,Java给出了哪些工具来保证原子性和线程安全?

内置锁

synchronized关键字。内置锁可以作用在方法、代码块中,作用在方法时表示用该类的当前实例(this)作为锁给方法体加锁。内置锁的实现是通过编译器加入monitor_enter和montior_exit指令,在虚拟机遇到前者时尝试获取锁,把锁的计数器加1;遇到后者时,将锁计数器减1,锁计数器为0时,锁被释放。

内置锁一度是java中进行同步的唯一方法,很多遗留方法还是使用了内置锁进行同步,比如著名的VectorCollections里面的同步包装器(如Collections.synchronizedMap(hashmap))等。

关于它和Lock的比较,详见此文。结论是,建议优先使用synchronized来进行同步。

显式锁

显式锁的顶层接口为Lock,提供了ReenterantLock, ReadWriteLock等实现。常见用法如下:

Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 方法体
} 
...
finally {
    lock.unlock();
}

所谓可重入就是锁的获得是以线程为单位的,同一线程获得锁后可以重复进入锁。锁会保存被持有的计数。

信号量、栅栏、闭锁

信号量Semaphore,相当于允许进入数量大于1的锁。
闭锁Latch,实现类CountDownLatch。相当于一个门,闭锁到达结束状态前,门一直关着,所有线程都不能通过。当闭锁到达结束状态时,门打开并允许所有线程通过。
栅栏Barrier,所有线程都等待时才打开放行。

CAS 与乐观锁

现代CPU支持一种CAS(Compare And Swap)指令,可以在一个指令内完成设置和冲突检测,从而实现了高效的原子性。CAS指令接受三个参数(v, expectedValue, newValue)。如果变量v的值和expectedValue相等,那么就将v赋值为newValue;如果和expectedValue不相等,就返回失败。

为何CAS的效率更高?采用互斥同步策略的最主要问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略(悲观锁),即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下文切换(由此导致内核态和用户态切换)导致效率很低。

而基于冲突检测(CAS)的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重试,直到试成功为止),这种乐观的并发策略不需要把线程挂起,因此这种同步被称为非阻塞同步。

Java 5.0之后才支持CAS,并用它实现了一些原子变量类,如AtomicInteger//AtomicReference等等。更重要的是,前面提到的所有锁机制几乎都使用了CAS来做性能优化。

线程间协作

这里的线程间协作是指通过一些机制使得线程可以彼此等待、唤醒,从而能够合作。例如,在生产者-消费者模型中,如果生产者向队列中放入一个新任务,可以立刻唤醒一个等待在此的消费者,这便是协作。

首先是基于内置锁和Object类的wait()notify()notifyAll()方法。
在java中,每个对象都有两个池,锁池和等待池。

锁池:要进入synchronized同步块的线程,如果此同步块的锁(是一个对象)被其他线程持有,则显然线程不能执行下去。线程将被放入该锁对象的锁池中,在锁池中的线程都在竞争这个锁。

等待池:调用了锁对象的wait()方法后,就进入了等待池。在等待池中的线程不去竞争锁,而是等待被锁对象的notify()notifyAll()唤醒,之后再进入锁池,开始竞争锁。

所以,锁池中的线程相当于睡着了,而等待池中的线程则进入了第二层睡眠!(如果你看过《盗梦空间》的话~)

再来讲这三个方法就好理解了:

Object.wait()
将当前线程放到锁的等待池,直到接到通知(其他线程调用 notify()方法或 notifyAll()方法)或被中断。在调用 wait()之前,线程必须要获得该对象锁,即只能在同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回时(被叫醒时),线程被放入锁池,与其他线程竞争重新获得锁。

Object.notify()
也必须在同步方法或同步块中调用,用来“叫醒”锁的等待池中的其他线程。如果有多个线程等待,则任意挑选出其中一个,扔到锁池中,但不惊动其他同样在等待被该对象notify的线程们。这里的“叫醒”只是叫醒第二层睡眠,还没完全醒。

Object.notifyAll()
把所有的锁等待池中的线程扔到锁池中。

说了这么多,这几个方法有什么用?还是生产者-消费者问题,队列数据的正确性需要同步机制来确保,而两个线程何时生产,何时取走就需要线程间协作了。详见此文。

最后,实际上这几个方法已经过时了。如果想实现等待阻塞的功能,应该使用更好用的LockCondition,与前面的组合如出一辙。

关于LockCondition的例子,见此回答。

可见性与同步、volatile

可见性指的是,一个变量被一个线程更改后,另一个线程在读取时由于时间顺序,可能得到的最新的有效值,也可能得到的是旧的无效值。

因此,同步的意义不仅仅在于写,还在于读。只要在读的时候也进行同步操作(加锁),就肯定能保证可见性。

另一方面,使用volatile关键字可以实现轻量级的可见性。volatile关键字会禁止所修饰的变量被指令重排序和优化成寄存器值从而不对所有线程可见。由于Java保证最低可见性(CPU设置一个变量会是个原子操作,不会出现设置到一半就被读取,从而得到一个随机值的情况),因而volatile可以实现非常高效的可见性。

但是volatile的局限也是有的:它只能用于赋值操作,如果是i++这种组合操作,结果依赖于之前的值,就不再能保证原子性了,因而无法保证准确。这时,只能采用加锁操作(或者CAS的冲突重试,总之要保证原子性)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68534.html

相关文章

  • 【J2SE】java并发编程实战 读书笔记( 一、、三章)

    摘要:发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。不变性线程安全性是不可变对象的固有属性之一。可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁保护起来。 线程的优缺点 线程是系统调度的基本单位。线程如果使用得当,可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。多线程程序可以通过提高处理器资源的利用率来提升系统的吞吐率。与此同时,在线程的使用...

    QLQ 评论0 收藏0
  • 阿里 2021 版最全 Java 并发编程笔记,看完我才懂了“内卷”的真正意义

    摘要:纯分享直接上干货操作系统并发支持进程管理内存管理文件系统系统进程间通信网络通信阻塞队列数组有界队列链表无界队列优先级有限无界队列延时无界队列同步队列队列内存模型线程通信机制内存共享消息传递内存模型顺序一致性指令重排序原则内存语义线程 纯分享 , 直接上干货! 操作系统并发支持 进程管理内存管...

    不知名网友 评论0 收藏0
  • Java并发编程的艺术】第一章读书笔记

    摘要:前言并发编程的目的是让程序跑的更快,但并不是启动更多的线程,这个程序就跑的更快。尽可能降低上下文切换的次数,有助于提高并发效率。死锁并发编程中的另一挑战是死锁,会造成系统功能不可用。 前言 并发编程的目的是让程序跑的更快,但并不是启动更多的线程,这个程序就跑的更快。有以下几种挑战。 挑战及方案 上下文切换 单核CPU上执行多线程任务,通过给每个线程分配CPU时间片的方式来实现这个机制。...

    马忠志 评论0 收藏0
  • 后台开发常问面试题集锦(问题搬运工,附链接)

    摘要:基础问题的的性能及原理之区别详解备忘笔记深入理解流水线抽象关键字修饰符知识点总结必看篇中的关键字解析回调机制解读抽象类与三大特征时间和时间戳的相互转换为什么要使用内部类对象锁和类锁的区别,,优缺点及比较提高篇八详解内部类单例模式和 Java基础问题 String的+的性能及原理 java之yield(),sleep(),wait()区别详解-备忘笔记 深入理解Java Stream流水...

    spacewander 评论0 收藏0
  • 后台开发常问面试题集锦(问题搬运工,附链接)

    摘要:基础问题的的性能及原理之区别详解备忘笔记深入理解流水线抽象关键字修饰符知识点总结必看篇中的关键字解析回调机制解读抽象类与三大特征时间和时间戳的相互转换为什么要使用内部类对象锁和类锁的区别,,优缺点及比较提高篇八详解内部类单例模式和 Java基础问题 String的+的性能及原理 java之yield(),sleep(),wait()区别详解-备忘笔记 深入理解Java Stream流水...

    xfee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<