热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java并发编程实战2线程安全

1.定义一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。2.WHY我们想要的是线程安全的程序,为什么在线程安全的开始讲线程安全的类呢?编写线程安全的代码,本质

1. 定义

一个类是线程安全的,是指在被多个线程访问时,类可以持续进行正确的行为。

2. WHY

我们想要的是线程安全的程序,为什么在线程安全的开始讲线程安全的类呢?

编写线程安全的代码,本质上就是管理对状态的访问,而且通常是共享的、可变的状态

我们讨论的的线程安全性,看起来好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中保护数据

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方不必作其它的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

对与线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态。

3. 无状态对象是线程安全的

无状态对象:它不包含域也没有引用其他类的域。

例如:

public class MathAdd {
	public int add(int a, int b){
		return a + b;
	}
}

对于MathAdd的实例来说,它只有一个计算两个int数值的的和的add()方法,每个执行线程在运行时,本地变量存储在线程栈中,只有执行线程能够访问,那么无论这个示例被多少个线程并发执行,不同的线程之间并不会相互影响,原因就是:两个线程不共享状态,它们如同在访问不同的实例。

4. 原子性

仍然使用MathAdd来说,假设我们现在需要统计一下,一个MathAdd类的实例被使用的次数,修改后的类如下:

public class MathAdd {

	private long count = 0;

	public long getCount(){
		return count;
	}

	public int add(int a, int b){
		++count;
		return a + b;
	}

}

此时,MathAdd的实例不再是无状态的对象了,因为其中增加了一个count属性,而多个线程之间又要在add()方法中对count属性进行操作,因此count属性被多个线程共享并操作。

那么这个对象是线程安全吗?不是,因为在add()方法中,有 ++count; 语句,正是这个语句导致对象不是线程安全的。++count; 的执行过程是:先获取当前count的值,然后对当前值加1,将新值写回count,也就是有三个操作,不是一个原子操作

我自己给原子操作下了一个定义:一个操作是不可分割的,就是原子操作。

5. 竞争条件

当计算的正确性依赖于运行时相关的时序或者多线程的交替时,就会产生竞争条件。也就是说,计算的正确性依赖时序,会产生竞争条件。

通过代码来进一步理解:

public class LazyInitRace {
	private ExpensiveObject instance = null;

	public ExpensiveObject getInstance() {
		if(null == instance){
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

上面的代码是常用的方法惰性初始化,由于某个对象的初始化代价比较昂贵(占用时间和资源),因此延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。

上述代码存在竞争条件:当两个线程操作同一个LazyInitRace对象时,A线程检查instace为空,于是new一个对象;这时,B线程也检查instance是否为null,如果A已经成功new了一个对象,那么B线程就直接返回A线程new的对象;但是,也存在这种情况,A正在new对象,但是没有完成,此时instance还是null,于是B线程也来new对象了,这时就存在两个instance对象了,可能直接导致后面所有的程序都是在两个不同的instance对象上操作,导致程序出错。

6. 复合操作

在MathAdd类和LazyInitRace类中,都是由于某个语句或某个语句块不是原子操作:++count;不是一个原子操作;if(null == instance) { instance = new ExpensiveObject(); }更是明显的非原子操作,也就是复合操作,但是我们在运行程序时,只有满足是原子操作操作时,才能保证运行结果的正确性。

正是由于多线程程序会用到复合操作的中间结果,导致了对象不是线程安全。对于多线程共用的对象来说,中间结果可能是正确的,也可能是不正确,任何一个线程都不能依赖于复合操作的中间结果。

俗语说“苍蝇不叮无缝的蛋”,对于多线程来说,Bug专叮依赖于复合操作中间结果的多线程,也可以说Bug专叮依赖于时序的多线程

7. 内部锁

通过前面的学习,我们发现导致多线程出错的原因,就是对象的某些操作不是原子操作,出现了中间结果,进而导致运行结果错误。因此,Java提供了强制原子性的内置锁机制:syschronized块,语法如下:

synchronized(loack) {
    //访问或修改被锁保护的共享状态
}

从synchronized的语法上可以看出,它有两部分:锁对象的引用,以及这个锁保护的代码块。

执行线程进入synchronized块前,需要获得锁,否则必须等待或阻塞;无论通过正常控制路径退出,还是从块中抛出异常,线程都会释放锁。以此,保证了原子性。实例代码:

public class LazyInitRace {
	private ExpensiveObject instance = null;

	public synchronized ExpensiveObject getInstance() {
		if(null == instance){
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

注意一点,如果用synchronized修饰方法,那么锁的对象会有不同:

(1) 当方法不是静态方法时,锁是该方法所在的对象本身;

(2) 当方法是静态方法时,锁是该方法所在的class的class对象。

每个Java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁

8. 可重入性

当一个线程获得了锁之后,当该线程再次请求获得该锁时,是可以再次成功获得该锁,就是可重入。从逻辑上来说,这是合理的,如果不满足可重入,会出现什么情况呢:

public class Parent {

	public synchronized void doSomething(){
		System.out.println("doSomething in Parent");
	}

}

public class Child extends Parent {

	public synchronized void doSomething(){
		System.out.println("doSomething in Child");
		super.doSomething();
	}

}

如果锁不满足可重入性,那么当在Child中调用doSomething时,执行到了super.doSomething()时,永远无法进入,造成死锁。

9. 用锁来保护状态

通过对对象内部的可变状态变量加锁,可以保证对状态变量的操作是原子的,也就保护了状态。

只要保证多线程访问时,每次都用同一个锁来保护变量,就能避免竞争条件。因此,这个锁是否与对象有关都没有关系。但是,为了便利,每个对象都有一个内部锁,所以不需要显式创建锁对象。

对于每一个设计多个变量的不变约束,需要同一个锁来保护其所有的变量。

10. 性能

synchronized块是好的,可以保证操作是原子的。但是,如果将整个方法都加上synchronized的话,那么这个方式就是串行的了,失去了多线程的好处。因此,synchronized块的大小要根据需要设置,甚至可以将不要保证原子操作的两部分代码分别加上synchronized,从而保证多线程的性能。示例代码:

public class Test {

	public void doSomething(){

		//代码1,不需要保证同步

		synchronized(this){
			//代码2,保证同步
		}

		//代码3,不需要保证同步

		synchronized(this){
			//代码4,需要保证同步,但是跟代码2中没有关联
		}

	}

}

推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
author-avatar
滞留童年车
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有