java并发安全解析

并发安全

类的线程安全定义:
如果多线程下使用这个类,不管多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。

类的线程安全表现为:
	1,操作的原子性
	2,内存的可见性

不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

怎么才能做到类的线程安全?

1,栈封闭:
所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态,都是线程安全的。

2,无状态:
没有任何成员变量的类,就叫无状态的类。

3,让类不可变:

	让状态不可变,两种方式:
		1,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
		2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。

4,volatile:
保证类的可见性,最适合一个线程写,多个线程读的情景。

5,加锁和CAS。

6,安全的发布:
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

Servlet:
	不是线程安全的类,为什么我们平时没感觉到:
		1、在需求上,很少有共享的需求。
		2,接收到了请求,返回应答的时候,都是由一个线程来负责的。

线程安全问题

死锁

资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。

死锁的根本成因:获取锁的顺序不一致导致。
有n个进程,共享的同类资源数为m,则避免死锁的最少资源数是n(m-1)+1。

		产生死锁的原因:
			1,因为系统资源不足
			2,进程运行推进的顺序不合适
			3,资源分配不当等
			
		产生死锁的四个必要条件:
			1,互斥条件:一个资源每次只能被一个进程使用
			2,请求与保持条件:一个进程因请求字眼而阻塞时,对已获得的资源保持不放
			3,不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
			4,循环等待条件:若干进程之间形成一种头尾相接的循环等到资源关系
			
		怀疑发送死锁:
			简单的死锁:
				1,通过jps 查询应用的 id,
				2,再通过jstack id 查看应用的锁的持有情况
				解决办法:保证加锁的顺序性
				
			动态的死锁:
				动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。
				例如:
					void test(Account fromAccount,Account toAccount){
						synchronized (fromAccount) {
					        synchronized (toAccount) {
					        }
					    }
					}
				虽然内部固定了加锁的顺序,但是外部传入的锁的顺序不一样,所以可能造成死锁。
				
				解决:
				1、	通过内在排序,保证加锁的顺序性(调用System.identityHashCode(obj)获取原始的hashcode值,再排序确定顺序,如果hash值一样了,就在外部再定义一个锁,让这两个线程去竞争一次这个锁,谁拿到锁再进入依次获取之前两个锁)
				2、	通过尝试拿锁,也可以,在传入的对象内部定义显示锁,然后自旋,依次调用tryAcquired,尝试拿锁,拿不到就自旋再获取:
				
						while(true) {
				    		if(from.getLock().tryLock()) {
				    			try {
				    				if(to.getLock().tryLock()) {
				    					try {
				    					}finally {
				    						to.getLock().unlock();
				    					}
				    				}
				    			}finally {
				    				from.getLock().unlock();
				    			}
				    		}
				    		SleepTools.ms(r.nextInt(10));//休眠一个随机数,否则会发生活锁。
				    	}
				    }
				    
				3、 银行家算法是一种最有代表性的避免死锁的算法。又被称为“资源分配拒绝”法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。为实现银行家算法,系统必须设置若干数据结构。

活锁

尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程,比如线程1尝试拿到A锁,尝试拿B锁失败,线程2尝试拿到B锁,尝试拿A锁失败,这样线程1和线程2都循环再次尝试获取两个锁,这时候又是之前的情况。

解决办法:每个线程休眠随机数,错开拿锁的时间。

线程饥饿

低优先级的线程,总是拿不到执行时间。

性能和思考

使用并发的目标是为了提高性能,引入多线程后,其实会引入额外的开销,如线程之间的协调、增加的上下文切换,线程的创建和销毁,线程的调度等等。过度的使用和不恰当的使用,会导致多线程程序甚至比单线程还要低。

衡量应用的程序的性能:服务时间,延迟时间,吞吐量,可伸缩性等等,其中服务时间,延迟时间(多快),吞吐量(处理能力的指标,完成工作的多少)。多快和多少,完全独立,甚至是相互矛盾的。

对服务器应用来说:多少(可伸缩性,吞吐量)这个方面比多快更受重视。

	我们做应用的时候:
		1、	先保证程序正确,确实达不到要求的时候,再提高速度。(黄金原则)
		2、	一定要以测试为基准。
	一个应用程序里,串行的部分是永远都有的。
	Amdahl定律  :  1/(F+(1-N)/N)   F:必须被串行部分,程序最好的结果, 1/F。

影响性能的因素

		1,上下文切换:
			是指CPU 从一个进程或线程切换到另一个进程或线程。一次上下文切换花费5000~10000个时钟周期,几微秒。在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
			上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
			
		2,内存同步:
			一般指加锁,对加锁来说,需要增加额外的指令,这些指令都需要刷新缓存等等操作。
			
		3,阻塞:
			会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作】。很明显这个操作包括两次额外的上下文切换。

优化性能

		1,减少锁的竞争
		2,减少锁的粒度:
			使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象。但是如果有同时要持有多个锁的业务方法,要注意避免发生死锁
			缩小锁的范围
			对锁的持有实现快进快出,尽量缩短持由锁的的时间。将一些与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作
		3,避免多余的缩减锁的范围:
			两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化—扩大锁的范围。
		4,锁分段:
			ConcurrrentHashMap就是典型的锁分段。
		5,替换独占锁:
			在业务允许的情况下:
				1、	使用读写锁,
				2、	用自旋CAS
				3、	使用系统的并发容器
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值