在前面我们介绍了JVM,在此基础上,我们可以进一步更好的了解多线程。接下来,我将会写一系列多线程的那些事,这些都是我对多线程的一个小总结,希望对大家学习和深入多线程带来帮助。
为什么需要并行:
– 业务要求
– 性能
– 并行计算还出于业务模型的需要
并不是为了提高系统性能,而是确实在业务上需要多个执行单元。
比如HTTP服务器,为每一个Socket连接新建一个处理线程
让不同线程承担不同的业务工作
简化任务调度
几个重要的概念
同步(synchronous)和异步(asynchronous) :
并发(Concurrency)和并行(Parallelism):
临界区:
– 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程 使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking):
– 阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要 这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如 果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。
– 非阻塞允许多个线程同时进入临界区
死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock) :
饥饿是指某一个或 者多个线程因为种 种原因无法获得所 需要的资源,导致 一直无法执行。
并行的级别:
– 阻塞
一个方法被称为阻塞的,即这个方法在其演进过程中不能正常运行直到其他(占有锁的)线程释放。
– 无障碍
无障碍是一种最弱的非阻塞调度
自由出入临界区
无竞争时,有限步内完成操作
有竞争时,回滚数据
– 无锁
是无障碍的
保证有一个线程可以胜出
while (!atomicVar.compareAndSet(localVar, localVar+1))
{ localVar = atomicVar.get(); }
– 无等待
无锁的
要求所有的线程都必须在有限步内完成
无饥饿的
2个重要的定理:
Amdahl定律(阿姆达尔定律):
– 定义了串行系统并行化后的加速比的计算公式和理论上限
– 加速比定义:加速比=优化前系统耗时/优化后系统耗时
假設要開發一個新任務,這個任務分為5步完成,每一步所需要消耗CPU 100毫秒的時間。如下圖:
第一條任務鏈是以單線程實現的,第二條任務線其中步驟二和步驟五是以2個線程實現的。
加速比=优化前系统耗时/优化后系统耗时=500/400=1.25
增加CPU处理器的数量并不一定能起到有效的作用 提高系统内可并行化的模块比重,合理增加并行处 理器数量,才能以最小的投入,得到最大的加速比。
Gustafson定律(古斯塔夫森):
– 说明处理器个数,串行比例和加速比之间的关系
只要有足够的并行化,那么加速 比和CPU个数成正比。
什么是线程:
– 线程是进程内的执行单元
线程的状态:
新建状态(New)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。
就绪状态(Runnable)
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。
运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。
阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。
死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
线程的基本操作
新建线程
Thread t1=new Thread(); t1.start();
Thread t1=new Thread(Runnable r); t1.start();
终止线程
– Thread.stop() 不推荐使用。它会导致临界区的最终数据不一致。
例如有2个线程需要同时读一个临界区的A对象(里面有a和b两个变量),然后根据读到的数据再决定后续操作。
当线程1读取了A对象,并根据业务操作需要对a和b变量做出修改,但修改了a之后被调用了Thread.stop(),此时b变量还没来得及修改,a变量又不能回滚修改的操作,所以导致线程2 读取到A对象的数据不一致。
中断线程
调用Thread.interrupt()方法对某一个线程中断之后,目标线程不会立刻停止运作,仅仅是对目标线程做了一个中断标记,需要目标线程调用isInterrupted()或者interrupted()方法判断当前线程是否需要中断了,然后做中断处理,例如数据回滚、数据备份。
或者当目标调用了sleep()方法之后,会抛出一个InterruptedException异常,可以在catch块中做中断处理。
public void Thread.interrupt() // 中断线程
public boolean Thread.isInterrupted() // 判断是否被中断
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态
挂起(suspend)和继续执行(resume)线程
– suspend()不会释放锁
– 如果加锁发生在resume()之前 ,则死锁发生
等待线程结束(join)和谦让(yeild)
join的本质
– while (isAlive()) { wait(0); }
守护线程
在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程
当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出
Thread t=new DaemonT();
t.setDaemon(true);
t.start();
线程优先级
高优先级的线程更容易再竞争中获胜
//线程类中定义好的优先级:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
//设置优先级:
Thread high=new HightPriority();
LowPriority low=new LowPriority();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
基本的线程同步操作
Synchronized
– 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的
– 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
– 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
Object.wait(),Obejct.notify()
– wait():会释放锁
– notify():随机唤醒wait队列中的其中一个,当前Object释放锁之后,被唤醒的Object重新获取锁。
JVM锁机制
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
Contention List:所有请求锁的线程将被首先放置到该竞争队列, 该队列是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。
Entry List:Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中
Wait Set:如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现EntryList为空则从ContentionList中移动线程到EntryList。
那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。