【Java基础知识 17】聊一聊同步代码块
一、什么是内置锁?
Java提供了一种内置的锁机制来支持原子性:同步代码块。
同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
synchronized(lock){
//访问或修改由锁保护的共享状态
}
- 1
- 2
- 3
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java的内置锁相当于一个互斥体,这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞。直到线程B释放这个锁。如果线程B永远不释放锁,那么A也将永远等待下去。
由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。并发环境中的原子性与事务应用程序中的原子性有着相同的含义,一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其它线程正在执行由同一个锁保护的同步代码块。
二、《Java核心编程》中探索~~什么是重入?
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会响应地递减。当计数值为0时,这个锁将被释放。
package com.guor.util;
public class Father {
public synchronized void say(){
System.out.println("我是父类");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
package com.guor.util;
public class Son extends Father {
public synchronized void say(){
System.out.println(toString()+"call say()");
super.say();
}
public static void main(String[] args) {
Son son = new Son();
son.say();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。在上面代码中,子类改写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。由于Father和Son中say方法都是synchronized方法,因此每个say方法在执行前都会获取Father上的锁。然而,如果内置锁不是可重入的,那么在调用super.say时将无法获得Father上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。
三、活跃性与性能
要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其它线程可以访问共享状态。
在获取与释放锁的操作上都需要一定的开销,因此如果将同步代码块分解得过细,那么通常并不好,尽管这样做不会破坏原子性。当访问状态变量或者在复合操作的执行期间,需要持有锁,但在执行时间较长的因数分解运算之前要释放锁。这样既确保了线程安全性,也不会过多地影响并发性,并且在每个同步代码块中的代码路径都“足够短”。
要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性、简单性、性能。
当使用锁时,你应该清楚代码块中实现的功能,以及在执行该代码块时是否需要很长的时间,无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性或性能的问题。
当执行时间较长的计算或者可能无法快速完成的操作时(例如网络IO或控制台IO),一定不要持有锁。
四、对象的共享
1、可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。在单线程环境中,如果向某个变量写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。这听起来很自然。然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难以接受。通常,我们无法保证执行读操作的线程能实时地看到其它线程写入的值,优势甚至是不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
2、非原子的64位操作
当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile
类型的64位数值变量(double和long
)。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile
类型的long
和double
变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile
类型的long
变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long
和double
类型的变量也是不安全的,除非用关键字volatile
来声明它们,或者用锁保护起来。
3、volatile变量
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其它处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,读取volatile变量就相当于进入同步代码块。然而,我们并不建议过度依赖volatile变量提供的可见性。如果在代码中依赖volatile变量来控制状态的可见性,通常比使用锁的代码更加脆弱,也更难以理解。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当满足以下所有条件时,才应该使用volatile变量。
- 当变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其它状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁。
五、给大家推荐一本Java经典书籍
《Java核心编程》,国外广受赞誉的Java经典教科书,畅销十余年,版本三次升级,好评如潮。
1、作者简介
John Dean,帕克大学计算机科学与信息系统系的副教授,拥有堪萨斯大学的计算机科学硕士学位和诺瓦东南大学的计算机科学博士学位,拥有Java认证,曾在业界担任软件工程师和项目经理,专门研究Java和各种web技术。
Raymond Dean,堪萨斯大学电气工程系和计算机科学系的名誉教授,拥有麻省理工学院的硕士学位和普利斯顿大学的博士学位。他同时也是暖通空调行业的专业工程师,编写过空气分配系统和建筑系统中能耗与声音传播的计算机程序。在堪萨斯大学,他教授微处理程序、数据结构以及电气工程和计算科学的课程。
这是一本优秀的Java技术图书,它以一种容易理解的语言介绍了Java语言的概念、算法设计和编程方法,这对于初学者学习和加强Java语言和面向对象的设计原则是非常好的。它从各个层次详细分析了Java的各个组成技术,并且利用大量的示例进行了技术的分析,不仅可以作为技术学习的图书使用,同时也可以作为工具字典使用。
2、本书适合谁?
- 适合在校大学生作为教程使用
- 适合有一些编程经验并想学习Java的行业从业者
- 适合各类Java自学者
- 高中阶段的计算机科学的预科生也推荐学习本书
3、本书特色
(1)解决问题
本书通过强调算法开发和程序设计两个关键元素讲解程序化地解决问题的方法。
(2)基本原理优先
本书将需要复杂语法的概念延后,为了高效地编写代码,通过程序示例代码追踪与说明来确保彻底理解代码
(3)贴近现实
本书通过上手实践与贴近现实的方式来学习Java编程,因而引入了以下资源:编译工具、完整的程序示例源代码、程序设计中的实践指导、基于行业标准的代码风格指南、用于类关系图的统一建模语言(Unified Modeling Language,UML)、分配的实践性家庭作业。
本书在撰写过程中,引导读者去了解三种重要的编程方法:结构化编程、OOP和事件驱动编程。
为激励读者学习有趣的Java编程,本书在一些章末穿插GUI跟踪部分,大多数章末部分的内容是使用GUI代码来完成此章前面部分展示的非GUI内容。
4、本书组织
在撰写本书的过程中,引导读者去了解三种重要的编程方法:结构化编程、OOP和事件驱动编程。
我们提倡的内容和顺序使学生能够在编程基本原理的坚实基础上发展他们的技能,这也是一名熟练的OOP程序员的最高效的顺序。为培养这种基本原理优先的方法,本教材从一组最少的概念和细节开始,逐步扩展概念并添加细节,把相对不是很重要的细节延后到之后的章节中,以避免前面的章节负担过重。
5、推荐:Java核心编程从问题分析到代码实现(第3版)(上下册)
下一篇:Java基础教程系列
文章来源: blog.csdn.net,作者:哪 吒,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/guorui_java/article/details/124648504
- 点赞
- 收藏
- 关注作者
评论(0)