Java线程与优化
»Java线程目录:
操作系统的线程实现
线程是一种比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行分开。操作系统实现线程主要有3中方式。
(1) 使用内核线程实现
内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度。程序一般不会直接使用内核线程,而是去使用内科线程的一种高级接口–轻量级进程,轻量级进程即我们通常所说的线程,并且这种轻量级进程与内核线程是一种一对一的关系,这也就是一对一的线程模型
由于是基于内核线程实现的,所以各种线程的操作,如创建、同步等都需要进行系统调用。而系统调用需要在内核态和用户态之间进行切换,代价较高。其次每个轻量级进程都对应一个线程,因此一个系统支持的轻量级进程数是有限的
(2) 使用用户线程实现
狭义上的用户线程是指完全建立在用户空间,用户线程的创建、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。因此操作非常快速并且低耗,也可以支持更大规模的线程数量。这种进程与用户线程之间一对多的关系称之为一对多的线程模型
使用用户线程的优势在于不需要系统内核支援,但是劣势也在于没有系统内核支援,所有的线程操作都需要用户程序自己处理。现在一般都不使用这种模型
(3) 使用用户线程加轻量级进程混合实现
这是一种将内核线程与用户线程一起使用的方式,在这种方式下既存在用户线程又存在轻量级进程,即用户线程通过轻量级进程与内核线程进行交互,即用户线程的系统调用要通过轻量级进程。用户线程与轻量级进程是多对多的关系。这就是多对多的线程模型。
Java线程实现
Java语言提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行start并且还未结束的Thread类的实例就代表一个线程。对于sun JDK来说,它的Windows和Linux版本都是使用一对一的线程模型实现的,即一条Java线程对应一条轻量级进程之中。
(1) java线程调度 线程调度是指处理器为线程分配处理器使用权的过程,主要调度方式有两种:
- 协同式线程调用:线程执行完之后才让位
- 抢占式线程调用:线程将由系统来分配执行时间
java实现的线程调度方式是抢占式线程调度。可以通过设置优先级来给某些线程多分配一些时间。不过优先级并不是太靠谱,原因是java的线程是通过映射到系统的原生线程上来实现的,所以线程的调度最终还是取决于操作系统,而操作系统的线程优先级未必会和java的线程优先级对应。同时,线程的优先级还有可能会被操作系统自行改变。
(2) 状态转换
Java语言定义了5种状态,任意时刻,一个线程只能对应其一种状态。
- 新建:创建后尚未启动的线程
- 运行:处于此状态的线程有可能正在运行、或者等待CPU为它分配时间
- 无限期等待:处于这种状态的线程不会被CPU分配时间,他们要等待被其他线程显式的唤醒
- 限期等待:处于这种状态的线程也不会被CPU分配时间,不过一定时间后他们会被操作系统自动唤醒
- 阻塞:阻塞状态与等待状态的区别是,阻塞状态在等待着获取一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生
- 结束:线程已经结束执行
线程安全
首先需要实现并发线程的安全性,然后在此基础上实现并发线程的高效。
当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
(1) java语言中的线程安全
按照线程安全的程度,可以将java语言中各种操作共享的数据分为5类
- 不可变
- 绝对线程安全
- 相对线程安全
- 线程兼容
- 线程对立
(2) 线程安全的实现方法
-
互斥同步(悲观的策略)
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号亮的时候)线程使用。而互斥是实现同步的一种手段。
在java中,最基本的互斥手段就是synchronized关键字,同步块在已进入的线程执行完之前,阻塞后面其他线程的进入,如果要阻塞或唤醒一个线程,都需要操作系统的来帮忙完成,这就需要从用户态转换到核心态,这个转换过程是比较耗时的。
除了使用synchronized,还可以使用ReentranLock重入锁来实现同步。相比synchronized,ReentranLock增加了一些高级功能:等待可中断,可实现公平锁,锁可以绑定多个条件。
能使用synchronized实现,就不要用ReentranLock,因为JDK对synchronized实现了很多优化。
-
非阻塞同步(乐观策略)
互斥同步最主要的问题就是进行线程阻塞和唤醒线程所带来的性能问题,因此这种同步也成为阻塞同步。
同时互斥同步属于一种悲观策略,即,它总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出问题,无论共享数据是否会发生竞争,他都会进行加锁。因此比较耗时
于是出现了一种基于冲突检测的乐观并发策略,简单说就是,先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施,常用的措施有,不断重试,直到成功(基于死循环的方式)。这种乐观的并发策略的许多实现都不需要将线程挂起,因此这种同步操作称为非阻塞同步。
CAS(Compare-and-swap):CAS指令需要三个操作数,分别是内存位置,旧的预期值,新值。当CAS指令执行时,当且仅当内存位置符合旧的预期值时,处理器才用新值更新内存位置。这一过程是原子操作
-
无同步方案
如果一个方法不涉及共享数据,那它自然就无须任何同步措施去保证正确性。例如使用线程本地存储ThreadLocal
锁优化
(1) 自旋锁与自适应锁 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成。
如果机器上有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程等一下,但是不是挂起它,而是看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只需让线程执行一个忙等待(自旋),这就是自旋锁
但是,自旋等待并不能代替阻塞,自旋本身虽然避免了线程切换的开销,但是它要占用处理器的事件,因此,如果自旋等待事件很短,那么效果就很好,等待时间很长,那么只会浪费处理器资源。因此,如果自旋超过了限定的次数(默认是10次),仍然没有成功获得锁,就应当使用传统方式去挂起线程。
在JDK1.6中引入了自适应自旋锁,自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
(2) 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
(3) 锁粗化
如果代码中连续反复都对同一个对象加锁和解锁,或者加锁操作出现在循环中,那么就需要把锁的范围扩大
(4) 轻量级锁
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。在有竞争的情况下,轻量级锁可能比重量级锁更慢
(5) 偏向锁