目录
基本概念
同步和异步:用来形容一次方法的调用
- 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
- 异步:异步方法通常会在另一个线程中“真实”地执行。整个过程不会阻碍调用者的工作。
并发和并行
- 并发:多个任务交替执行,而多个任务之间有可能还是串行的。
- 并行:多个任务“同时执行”。注:真实地并行只可能出现在拥有多个CPU的系统中。
临界区
- 临界区:共享数据,可以被多个线程使用,但是每次只能有一个线程使用。
阻塞和非阻塞:用来形容多线程间的互相影响
- 阻塞:如果一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在临界区中等待。
- 非阻塞:没有一个线程可以妨碍其它线程执行。
死锁、饥饿和活锁:多线程的活跃性问题
- 死锁:指多个进程在运行过程中因争夺资源而造成相互等待。
- 饥饿:一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
- 活锁:线程主动将资源释放给他人使用,出现资源不断在两个线程中跳动,而没有一个线程可以拿到所有资源而正常执行。
并发级别
- 阻塞:一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。
- 无饥饿:对于非公平的锁来说,系统允许高优先级的线程插队,可能会导致低优先级线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生。
- 无障碍:这是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么它们不会因为临界区的问题导致一方被挂起。如果发现数据异常,就会对之前的操作进行回滚,确保数据安全。当临界区资源存在严重冲突时,所有的线程可能都会不断回滚自己的操作,而没有一个线程可以走出临界区。可行的无障碍策略:“一致性标记”。操作之前,线程先读取并保持这个标记,操作完成之后,再次读取看释放别更改过,如果两者一致,说明资源访问没有冲突,如果不一致,说明资源可能在操作过程中与其他写线程冲突,需要回滚重试。并且,任何对资源有修改操作的线程,都需要在修改数据之前,更新这个一致性标记,表示数据不再安全。
- 无锁:无锁就是无障碍,同时保证了必然有一个线程能够在有限步内完成操作离开临界区。在无锁的调用中,可能会包含一个无限循环。循环中,线程会不断尝试修改共享变量。而对于竞争失败的线程,可能会出现饥饿的情况。
- 无等待:无等待在无锁的基础上进一步扩展,它要求所有线程都必须在有限步内完成,这样就不会引起饥饿问题。RCU(Read-Copy-Update)是一种典型的无等待结构:所有的读线程都是无等待,但在写数据时,先取得原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机回写数据。
线程的基本状态
- 新建( new ):新创建了一个线程对象。
- 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。
- 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
- 阻塞( blocked):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种: 等待阻塞,运行( 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 )状态。
- 死亡( terminated):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程的基本操作
实现线程:继承Thread类或实现Runnable接口
终止线程:Thread.stop(),这是一个已经被被标注为废弃的方法,用stop()方法强行把执行的线程终止,可能会引起一些数据不一致的问题。它在结束线程时,会直接终止线程,并且会释放这个线程所持有的锁。
public class MyThread implements Runnable{
volatile boolean stop = false;
public void stopMe(){//增加一个方法来自行决定线程何时退出
stop = true;
}
@Override
public void run() {
while(true){
if(stop)
break;
synchronized (u){
...
}
}
}
}
线程中断:线程中断并不会使线程立即退出,而是设置中断标志位,至于目标线程如何处理,则完全由目标线程自行决定。
public void Thread.interrupt() //中断线程
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
注意:如果Thread.sleep()方法由于中断而抛出异常,此时它会清除中断标记,应该在异常捕获中再次设置中断标志,以免无法捕获这个中断。
等待(wait)和通知(notify):object.wait()和object.notify()都需要首先获得目标对象的一个监视器
- 如果一个线程调用了object.wait(),那么它会进入object对象的等待队列
- 当object.notify()被调用时,它会从这个等待队列中随机唤醒一个线程(这个随机是不公平的,并不是先等待的会先选择)
- 当object.notifyAll()被调用时,它会唤醒这个等待队列中的所有等待的线程
object.wait()和Thread.sleep()的区别:
- object.wait():可以被唤醒,并且会释放目标对象的锁
- Thread.sleep():不会释放任何资源
挂起(suspend)和继续执行(resume)线程:suspend()和resume()也是被被标注为废弃的方法
- suspend()在导致线程暂停的同时,并不会释放任何锁资源,直到对应的线程进行了resume()操作,被挂起的线程才能继续
- 如果resume()意外地在suspend()前执行,那么被挂起的线程很难有机会被继续执行,并且它所占用的锁也不会被释放,因此可能导致整个系统工作不正常。而且,对于被挂起的线程的状态为Runnable,严重影响我们对系统当前状态的判断
等待线程结束(join)和谦让(yield)
- join():阻塞当前线程,直到目标线程执行完毕。join的本质是让调用线程wait()在当前线程对象实例上,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()通知所有等待线程继续执行。因此,不要在Thread对象实例上使用类似wait()或者notify()等方法。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
- Thread.yield():一旦执行,它会使当前线程让出CPU,但是当前线程还是会进行CPU资源的争夺。如果一个线程不是那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。
public static native void yied();
volatile与Java内存模型(JVM)
java内存模型是围绕原子性、可见性和有序性展开的。volatile关键字修饰一个变量:
- 保证数据的可见性和有序性:在虚拟机的Client模式下,主线程修改变量其它线程可以发现变动;但在Server模式下,由于系统优化的结果,其它线程无法看到主线程的修改。用volatile关键字修饰变量,就可确保这个变量被修改后,应用程序范围内的所有线程都能看到这个改动。
- volatile对于保证操作的原子性有很大的帮助,但无法保证原子性操作。
线程组
在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
public class MyThread implements Runnable{
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("printGroup");
Thread t1 = new Thread(tg,new MyThread(),"T1");//通过Thread的构造函数指定线程所属的线程组
Thread t2 = new Thread(tg,new MyThread(),"T2");
t1.start();t2.start();
tg.activeCount();//获得活动线程总数,但是线程是动态的,这个值是一个估计值
tg.list();//可以打印这个线程组中所有的线程信息,可帮助调试
}
@Override
public void run() {
}
}
注:1. 不要使用stop(),他会停止线程组中的所有线程,但它会遇到和Thread.stop() 相同的问题;
2. 在创建线程组和线程的时候,尽量给它们取有意义的名字。
守护线程(Daemon)
守护线程是一种特殊的线程,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的是用户线程,用户线程是系统的工作线程,它会完成这个程序应该完成的业务操作。当一个java应用内,只有守护线程时,java虚拟机会自然退出。
注意:设置守护线程必须在线程start()之前,否则会抛出异常但程序依然能正常执行,该线程会被当作用户线程。
thread.setDeamon(true); //设置thread为守护线程
线程安全和synchronized关键字
线程安全:在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。
synchronized用来实现线程之间的同步。它的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块。
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例对象的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。