Java并发概述

并发与并行

  • 并发:是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。 在一个时间段的线程代码运行时,其它线程处于挂起状。
  • 并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,cpu同时处理这些线程请求的能力。

所以在并发环境下,程序的封闭性被打破,出现以下特点:

  1. 并发程序之间有相互制约的关系。直接制约体现为一个程序需要另一个程序的计算结果;间接体现为多个程序竞争共享资源,如处理器、缓冲区等。
  2. 并发程序的执行过程是断断续续的。程序需要记忆现场指令及执行点。
  3. 当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率。

在并发环境中,当一个对象可以被多个线程访问到时,会造成该对象可以被任何访问到的线程进行修改,从而出现数据不一致的情况。所以提出线程安全的概念。

线程基本概念介绍

线程与进程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位) 。简单讲进程就是在某种程度上相互隔离的、独立运行的程序。

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 image

  1. 创建: 新创建了一个线程对象,还未调用start()方法。 如 Thread thread = new Thread();
  2. 就绪: 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取cpu 的使用权 。
  3. 运行: 运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
  4. 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域(synchronized)的时候,线程将进入这种状态。
(一). 等待阻塞: 运行(running) 的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

(二). 同步阻塞: 运行(running) 的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

(三). 其他阻塞: 运行(running) 的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入 可运行(runnable) 状态。
  1. 等待: 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
阻塞:当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。
等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。例如调用:Object.wait()、Thread.join()以及等待Lock或Condition。
  1. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  2. 终止(TERMINATED):表示该线程已经执行完毕。

线程安全

线程安全:多个线程访问某个类,这个类始终都能表现出正确的行为。可以理解为一个对象可以完全的被多个线程同时使用我们称之为线程安全的。

线程安全等级

按照线程安全的"安全程度"由强至弱来排序,我们可以将java语言中的各种操作共享数据的分为五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变
    在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。
  2. 绝对线程安全
    绝对的线程安全完全满足BrianGoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。在javaAPI中标注自己是线程安全的类,大多数都不是绝对线程安全的。如 Vector hashTable等。
  3. 相对线程安全
    相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。 它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在java语言中,大部分的线程安全类都属于相对线程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保证的集合。
  4. 线程兼容
    线程兼容就是我们通常意义上所讲的一个类不是线程安全的。线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
  5. 线程对立
    线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。 一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因此如此,这两个方法已经被废弃啦。

如何实现线程安全

实现线程安全通过是否需要同步分为两大类;首先解释下什么是同步。
同步: 指多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。

  • 互斥同步
    实现同步的方法一般是互斥。如临界区、互斥量、信号量等。因此互斥是因,同步是果;互斥是方法、同步是目的。
    Java互斥手段:synchronized、JUC下的ReentrantLock。
  • 非阻塞同步
    随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
  • 无需同步的方案
    要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1.  可重入代码:这种代码也叫做纯代码(pure code),可以在代码执行的任何时刻中断他,转而去执行另外的代码,而在控制权返回后,原来的程序不会发生任何错误。个人理解:这段代码不会存储任何的共享可变变量,只做处理逻辑。
2.  本地存储(ThreadLocal)   
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经
posted @ 2019-01-16 22:49  LotorLess  阅读(3400)  评论(0编辑  收藏  举报