本篇博客仅做学习记录,参考廖雪峰Java教程和多篇优秀博客,侵删!
进程与线程
概念
进程:在操作系统中可以独立运行,作为资源分配的基本单位,表示运行中的程序。
线程:进程中的一个实例,作为系统调度和分派的基本单位,是进程中的一段序列,能够完成进程中的一个功能。
进程和线程的关系
一个进程可以包含一个或多个线程,但至少会有一个线程。
线程与进程的区别
- 创建进程比创建线程开销大,尤其在Windows系统上;
- 进程间通信比线程间通信要慢;
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;
- 线程不能够独立执行;
- 线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
线程的状态
Java线程的状态
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行run()方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
- Terminated:线程已终止,因为run()方法执行完毕。
线程终止的原因:
- 线程正常终止:run()方法执行到return语句返回;
- 线程意外终止:run()方法因为未捕获的异常导致线程终止;
- 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。
join()
当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。
对已经运行结束的线程调用join()方法会立刻返回
中断线程
interrupt()
在其他线程中对目标线程调用**interrupt()方法中断线程,目标线程需要通过isInterrupted()**方法反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt(),join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted() && n<12) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。
设置标志位
running标志位来表示线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束。
标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
volatile关键字
volatile关键字是用来保证有序性和可见性的,但是不能保证原子性。
有序性:
我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU也会做重排序的,这样的重排序是为了减少流水线的阻塞的,引起流水阻塞,比如数据相关性,提高CPU的执行效率。
需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有happens-before规则,其中有条就是volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
对于写操作:对变量更改完之后,要立刻写回到主存中;
对于读操作:对变量读取的时候,要从主存中读,而不是缓存。
有序性实现的是通过插入内存屏障(见下)来保证的。
可见性:
在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的。
也就是说当一个线程修改某个共享变量的值时,另一个线程可能不知道,并且读取了旧的值。
因此,volatile关键字的目的是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存。
内存屏障
参考链接1,参考链接2。
一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏。
内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障
内存屏障提供了两个功能。
首先,它们通过确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,而保持程序顺序的外部可见性;
其次它们可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统。
守护线程(Daemon Thread)
概念
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
创建
创建守护线程方法和普通线程一样,只是在调用start()方法前,调用**setDaemon(true)**把该线程标记为守护线程。
Thread t = new MyThread();
t.setDaemon(true);
t.start();
守护线程不能持有需要关闭的资源(如打开文件等)
线程同步
原子性
概念
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
在多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。
JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List list = anotherList。
synchronized关键字
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁。
synchronized(lock) {
n = n + 1;
}
我们来概括一下如何使用synchronized:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用synchronized(lockObject) { … }。
死锁
Java的线程锁是可重入的锁。
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。
相关面试题
面试题内容来源于牛客上分享的一些面经。
请你谈谈关于Synchronized和Lock
synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁tryLock(),而synchronized却无法办到。
请你介绍一下Syncronized锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?
synchronized修饰静态方法以及同步代码块的synchronized (类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
synchronized修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。