1. 区分概念:并发和并行
- 并发是逻辑上的同时发生,并行更多是侧重于物理上的同时发生。
- 并发往往是指程序代码的结构支持并发,并发的程序在多cpu上运行起来才有可能达到并行,并行往往是描述运行时的状态。
- 在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
- 并发关注的三个问题:
- 安全性,也就是正确性,指的是程序在并发情况下执行的结果和预期一致
- 活跃性,比如死锁,活锁
- 性能,减少上下文切换,减少内核调用,减少一致性流量等等
2. 线程定义
- 进程:运行中的程序。
- 线程:是进程中的一个实体,作为系统调度和分派的基本单位。
- 现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process,简称LWP),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
当一个java程序从main()方法开始执行的时候,即启动了一个名字为main的线程。
3. 多线程的优势以及风险
优势:
- 更好的利用多处理器核心
- 缩短响应时间,提升用户体验
- 多线程为开发人员提供更好的编程模型
风险:
- 安全风险:资源竞争造成的安全问题
- 活跃度的危险:某部分代码永远不执行,例如死锁、饥饿、活锁
- 性能的风险:上下文切换的开销
线程安全问题:
- 线程安全的本质,其实是共享变量,也就是状态,有状态的多线程访问就需要同步机制来保证线程安全。
- 如果多线程访问一个类时,若该类在没有额外的同步代码时,行为仍然是正确的,则这个类时线程安全的。例如servlet这种无状态的类永远是线程安全的(servlet没有任何域(成员变量),只有存储在每个线程栈中的局部变量,所以是无状态的)
4. 守护线程(Daemon)和用户线程(User)
- 守护线程(后台线程)的定义:
是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程 - 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);参数为true则把该线程设置为守护线程,反之则为用户线程;Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
- Main线程是非守护线程,即用户线程。
- 守护线程与用户线程的区别
唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。
5. 使用线程
有三种使用线程的方法
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Tread 类;
1. 实现Runnable接口
public class MyRunnable implements Runnable {
public void run() {
// ...
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Tread thread = new Thread(instance);
thread.start();
}
}
2. 实现 Callable 接口
- 与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
3. 继承 Tread 类
- 同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
class MyThread extends Thread {
public void run() {
// ...
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
5. 实现接口 vs 继承 Thread
- 实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口。
- 类可能只要求可执行即可,继承整个 Thread 类开销会过大。
6. 线程相关易错点
- 启动线程通过在线程的Thread对象上调用start()方法,而不是run()或者别的方法。
- 在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。
- 在调用start()方法之后:发生了一系列复杂的事情
启动新的执行线程(具有新的调用栈);该线程从新建状态转移到可运行状态;当该线程获得机会执行时,其目标run()方法将运行; - 注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。
6. 线程的生命周期与调度
1. 线程的生命周期
- NEW(新建)
- RUNNABLE(当线程正在运行或者已经就绪正等待 CPU 时间片)(正在运行和就绪放用同一个状态表示)
- BLOCKED(阻塞,线程在等待获取对象同步锁)
- WAITING(调用不带超时的 wait() 或 join())
- TIMED_WAITING(调用 sleep()、带超时的 wait() 或者 join())
- TERMINATED(死亡)
2. 线程休眠sleep
- Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
- sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
public void run() {
try {
// ...
Thread.sleep(1000);
// ...
} catch(InterruptedException e) {
System.err.println(e);
}
}
2. 线程让步yield
- Thread.yield() 方法,让线程由运行状态变为就绪状态,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
- 实际上yield的流程是: 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU的占有权交给此线程,否则,继续运行原来的线程
public void run() {
// ...
Thread.yield();
}
2. 线程合并join
- join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- Join常用来让主线程等待子线程执行完毕才继续执行,例如下面这个例子:
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
- 打印结果如下:join挂起后,主线程要等到子线程结束后才继续运行
main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
3. 线程中断interrupt
涉及到的三个方法:
- void interrupt() : 中断线程
- static boolean interrupted() : 测试当前线程是否已经中断
- boolean isInterrupted() : 测试线程是否已经中断(注意与上一个区分)
interrupt()的实际用处:(容易误解)
- 不要以为它是中断某个线程!它只是让线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
- 代码实例如下:
public class InterruptTest{
public static void main(String[] args) throws Interruption {
MyThread t = new MyThread("MyThread");
t.start();
Thread.sleep(100);//睡眠100毫秒
t.interrupt();//通知线程t中断
}
}
class MyThread extends Thread{
int i = 0;
public MyThread(String name){
super(name);
}
public void run(){
while(!isInterrupted()){
System.out.println(getName() + "执行了" + ++i + "次");
}
}
}
- 通过interrupt()方法,线程t收到中断信号,设置中断状态,接着在0.1秒后响应中断而停止
- 注意:如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,并抛出一个 InterruptedException。 我们可以捕获该异常,并且做一些处理。
- 另外,Thread.interrupted()方法是一个静态方法,它是判断当前线程的中断状态,需要注意的是,线程的中断状态会由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
- interrupt()有设置中断状态的作用,对于没有被阻塞的线程,通过isInterrupt()判断它可以起帮助我们自行结束代码;对于被阻塞的线程,它可以让线程从停止阻塞,并抛出InterruptedException异常。
7. 线程之间的通信
以下几个方法属于基类(Object)的一部分,而不独属于 Thread,注意要与线程调度那几个方法区分开。
1. 线程等待wait
- wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。
- sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。
2. 线程唤醒notify与notifyAll
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
Wait和sleep经典示例:三个线程交替 打印ABCABC。。。
public class MyThreadPrinter2 implements Runnable {
private String name;
private Object prev;
private Object self;
private MyThreadPrinter2(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();//唤醒当前被self对象锁阻塞的线程
}
try {
prev.wait();//让当前持有prev锁的(自己)线程阻塞掉,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
- 程序运行的主要过程:A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。
3. wait、sleep、yield方法的区别
- sleep()是Thread类的static(静态)的方法;wait()方法是Object类里的方法;有一个易错的地方,当调用t.sleep()的时候,会暂停线程t。这是不对的,因为Thread.sleep是一个静态方法,它会使当前线程而不是线程t进入休眠状态。
- sleep()睡眠时,保持对象锁,仍然占有该锁;wait()睡眠时,释放对象锁
- 在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级;
- wait()必须放在同步代码块中,否则会在runtime时扔出IllegalMonitorStateException异常; 而sleep()没有这个要求,但是也可能会抛出InterruptedException异常。
- yield()是也是属于Thread类的方法,让线程由运行状态变为就绪状态,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程,这就意味着yield不会让调用者阻塞,而是继续保持就绪状态,甚至继续运行。