多线程问题及解决方法

  本文属于多线程的学习笔记,并不成完整的只是体系

安全性问题

  首先需要确认的是: 线程安全问题都是由全局变量及静态变量引起的,即共享资源。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

  • 线程安全性(定义):当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步和协同,
  • 这个类都能表现出正确的行为,那么就称这个类的线程安全的。
  • 竞态条件:当多个线程竞争操作相同资源时,其中访问资源的顺序是重要的,称为竞态条件。
  • 临界区:导致竞态条件的代码部分称为临界区。临界区用于表示公共资源或共享数据,可以被多个线程使用,但任意时刻只有一个线程使用它,一旦临界区的资源被占用,其他线程想要使用这个资源,就必须等待。

非安全代码

  • 无状态对象:无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象。不能保存数据,是不变类,是线程安全的。具体来说就是只有方法没有数据成员的对象,或者有数据成员但是数据成员是可读的对象。
  • 有状态对象:有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。其实就是有数据成员的对象。

不变模式

不变模式的核心就是对象不变,从而引伸出对象复用共享的思想。
如无状态的单例模式,享元模式(Flyweight)及原型模式(Prototype)都可以认为是不变模式的应用。其它如线程池,缓存等的实现也一定程度上是使用不变模式。还有EJB的Stateless Session Bean(无状态会话bean),Spring对Service层、Dao层bean的默认单例实现。Struts2默认的实现是Prototype模式.

弱不变模式

强不变模式

活跃性问题

安全性的含义是“永远不发生糟糕的事情”,而活跃性则关注于另一个目标,即“某件正确的事最终会发生”。
——by《Java 并发编程实战》

死锁

多个线程相互持有对方的资源,导致程序无法继续运行。

饥饿

一个或多个线程因为种种原因无法获取所要的资源,导致一致无法执行。(例如线程优先级太低)

活锁

多个线程间相互“谦让”资源,使得没有一个线程拿到所有资源而正常进行。

性能问题

  在设计良好的并发应用中,线程能提升程序的性能,但无论如何,线程总会带来某种程度的运行时开销。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作(Context Switch), 这种操作将带来极大的开销;保存和恢复上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会印制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。所有这些因素都将带来额外的性能开销。
——by 《Java 并发编程实战》

并发与并行

并行:指多个任务在同时执行,并且在任意时刻都是多个任务在执行
并发:指多个任务在同个时间段执行,但在这段时间内的任意时刻只有一个任务在执行(即切换任务执行)

远程方法调用(RMI)

  当RMI调用远程对象时,这个调用将在一个由RMI管理的线程中调用对象。同一个远程对象上的同一个远程方法会在多个RMI线程中被同时调用。

  • Out,Request,Response,Session,Config,Page,PageContext是线程安全的,Application在整个系统内被使用,所以不是线程安全的.

关于Synchronized关键字

  关于Synchronized关键字的一些笔记和收集。
synchronized底层实现原理:

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,……(点击下面链接了解详情)

synchronized底层实现原理,图文并茂-版

Synchronized的两种用法

synchronized的锁,原理上是利用每个对象关联的monitor,区别在于普通对象锁和class对象锁。

修饰实例方法或synchronized(普通对象)

锁住的是被修饰的方法的调用者对象(this)。所以,同类的不同实例对象(即锁的是不同的对象的monitor)在调用实例方法时,是不会线程同步的。

修饰静态方法或synchronized(静态属性)

锁住的是方法所在的类的字节码文件对象。(即存储在方法区(元空间)中的类信息的 Class 对象)这意味着,整个jvm中的线程都要排队执行,因为class对象在jvm是唯一的。

并发级别

由于临界区的存在,多线程之间的并发必须收到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上分为阻塞、无饥饿、无障碍、无锁、无等待等几种。–by《Java高并发程序设计》

阻塞

即使用synchronized或重入锁时,没有得到资源(锁)的线程被挂起等待,知道占有资源为止。

无饥饿

所有的线程平等,排队执行。(参考 饥饿)

无障碍

无障碍是一种最弱的非阻塞调度,即多个线程不会因为临界区的问题,而导致等待,而是共同操作资源,通过对资源的检测(如:检测一致性标记)、回滚,来保证数据的正确性。

无锁

所有的线程都可以对临界区进行访问,但是对于写操作,如果操作失败,就必须重新执行,直到执行成功。

一个典型的特点是可能会包含一个无穷循环 -by《Java高并发程序设计》

1
2
3
while(!atomicVar.compareAndSet(localVar, localVar+1)){
localVar = atomicVar.get();
}

无等待


对数据的读不加控制,因此称为无等待。而对于写操作,先取得原始数据副本,修改副本,在将副本写回。

关于volatile修饰的变量自增操作

首先是自增操作的执行步骤:

  1. 获取共享数据的副本
  2. 修改副本
  3. 将副本赋给原有的共享数据(物理地址)

volatile的特点在于内存屏障,即在写回数据后,或使得在其他线程内存中数据失效,迫使其他线程从新从临界区获取新的值。
实际上这一个一个类无等待的并发级别操作过程,即读无锁。

原子性

指一个动作不可中断,必然会完成。

zcolder wechat
写得不好?加我QQ开始喷我!