基础概念
同步和异步
同步和异步通常用来形容一次方法调用。
-
同步方法调用:一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
-
异步方法调用:更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作,而异步方法通常会另在另一个线程中 真实地执行,如果需要返回结果,那么当异步调用真实完成时,则会通知调用者。
并发与并行
- 并发:偏重于多个任务,交替执行,多个任务之间可能还是串行的。
- 并行:是真正意义上的同时执行。
临界区
用来表示一种 公共资源,或是共享数据,一旦临界区资源被某个线程占用,其他线程想要使用这个资源就必须 等待。
阻塞和非阻塞
通常用来形容多线程间的相互影响,
-
阻塞:如一个线程占用的临界区资源,那么其他需要这个资源的线程就必须在这个资源的临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。
-
非阻塞:强调没有一个线程能够妨碍其他线程执行。
死锁
多个线程持有彼此需要的资源不肯释放。
饥饿
线程因为种种原因无法获得其所需的资源,导致一直无法执行。
线程 优先级 过低,其它高优先级线程一直占有资源。
某一个线程一直占用关键资源不肯释放。
活锁
多个线程秉持 谦让 的原则,主动将资源释放给对方使用,导致资源不断在多个线程之间跳动,没有一个线程能同时拿到所有资源而正常执行。
并发级别
- 阻塞:在所需资源被释放前,当前线程无法继续执行。
- 无饥饿:锁是 公平 的,满足先来后到,不允许高优先级线程插队优先获取资源。
- 无障碍:允许多个线程进入临界区,无障碍的线程一旦检测到数据修改冲突,则对自己的修改进行 回滚,确保数据安全,进行重试。如果没有发生数据竞争,则可以完成修改操作。
- 无锁:CAS (Compare And Set),线程可能会通过一个无线循环来尝试修改变量,如果没有冲突则修改成功,否则继续尝试修改。
无等待
无等待要求所有线程在有限步内完成。通过限制步骤上限来避免饥饿问题
内存模型(Java Memory Model)
为了保证并发程序争正确的执行,就有了:
原子性
原子性指一个操作是不可中断的,即使在多线程的情况下也不会被干扰。
可见性
可见性指当一个线程修改了一个共享变量的值,其它线程是否能够知道这个修改。
有序性
程序在执行时,有可能会进行指令重排,CPU 指令执行的顺序不一定和程序的顺序一致。指令重排保证 串行语义一致
(即重排后执行的指令与程序真正执行顺序的语义一致),但不保证多线程语义一致。
并行程序基础
线程创建
- 继承
Thread
类 - 实现
Runnable
接口 - 实现
Callable
接口
线程停止
使用线程的 stop()
方法可以立即停止线程,但是是在线程执行过程中停止,容易造成数据不一致的问题,不建议使用。
线程中断
使用线程的 interrupt()
方法通知线程中断,收到中断通知的线程不会立即退出,而是由线程自行决定退出时机,为了避免造成数据不一致的问题,通过 isInterrupted()
方法获取线程的中断状态。wait()
、sleep()
等方法被中断会抛出 InterruptedException
异常,此异常会清除线程的中断标记。
等待(wait)和通知(notify)
Object
类提供了 wait()
和 notify()
方法,意味任何对象都能调用这两个方法, wait()
方法必须包含在 synchronized
语句块中,只有获取了目标对象的监视器,才能调用这两个方法。当在一个对象实例上调用 wait()
方法,当前线程就会进入目标对象的等待队列,此队列可能会包含多个等待目标对象的线程,当 notify()
方法被调用,就会从等待队列中随机唤醒一个线程,继续执行。当对象的 notifyAll()
方法被调用,等待队列中的所有对象都会被唤醒。
wait() 和 sleep() 的区别
wait()
方法可被唤醒,sleep()
方法只能一直休眠到指定时间。wait()
方法会释放目标对象的锁,sleep()
方法不会释放任何资源。
挂起(suspend)和继续执行(resume)
使用线程的 suspend()
方法会使线程暂停,但是并不会释放任何锁资源,直到执行此线程的 resume()
方法,如果 resume()
方法先于 suspend()
方法执行,那么线程就无法被唤醒从而继续执行,也不会释放占有的锁资源,而从线程状态上看,居然还是 RUNNABLE
,所以不建议使用挂起的方式去操作线程。
等待结束(join)
在某些时候,一个线程的输入可能会依赖其它线程的输出,这就需要通过使用被依赖线程的 join()
方法加入当前线程,等待被依赖线程执行结束。
谦让(yield)
使用线程的 yield()
方法会使线程让出 CPU,但不是暂停执行,线程还会进行 CPU 的争夺。如果一个线程不那么重要,或者对应任务的优先级非常低,或者不希望它占用过多的资源,就可以使用 yield()
方法给予其它线程更多的执行机会。
线程组
使用线程组可以对线程进行分组,从而方便管理。
ThreadGroup tg = new ThreadGroup("tg");
Thread t1 = new Thread(tg, "T1");
守护线程(daemon)
守护线程是一种特殊的线程,是系统的守护者,在后台默默完成一些系统性的服务,如垃圾回收、JIT 等。如果用户线程全部结束,那么整个应用程序也应该结束,守护线程也会退出。
Thread t = new Thread();
// 必须在执行 start() 方法之前设置为守护线程
t.setDaemon(true);
t.start();
线程优先级
Java 中的线程可以有优先级,优先级高的线程在竞争资源时会更有优势。线程优先级的范围为 1~10 ,数字越大优先级越高。
/**
* 线程可以拥有的最小优先级。
*/
public final static int MIN_PRIORITY = 1;
/**
*分配给线程的默认优先级。
*/
public final static int NORM_PRIORITY = 5;
/**
* 线程可以拥有的最大优先级。
*/
public final static int MAX_PRIORITY = 10;
volatile
使用 volatile
关键字可以通知虚拟机不能随意变动、优化目标指令,被此关键字修饰的变量被修改后,应用程序范围内所有的线程都能“看到”这个改动。volatile
关键字主要作用包括:
- 保证可见性:对修饰变量的修改能为其它线程所知,但是写冲突仍可能导致错误。
- 保证有序性:不能随意变动、优化指令。
synchronized
当多个线程同时写一个数据时,容易产生写冲突,Java 提供了用于线程间同步的关键字 synchronized
,它的作用是对同步代码进行加锁,使得每次只能有一个线程进入同步代码块,从而保证线程间的安全性,synchronized
关键字有多种用法:
- 指定加锁对象:对给定对象加锁,进入同步代码块前需要获取给定对象的锁。
- 直接作用于实例方法,相当于对当前实例加锁,进入同步代码块前需要获取当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码块前需要获取当前类的锁。
synchronized
限制每次只有一个线程可以访问同步代码块,因此即使进行指令重排,串行语义是不会改变的,被synchronized
限制的多个线程又可看做是顺序执行的,能够保证可见性和有序性。