基本概念
进程和线程
首先需要了解下进程和线程的区别:
- 进程其实就是程序,每个进程中的代码和数据空间(进程上下文)都是独立的,进程间的切换会有较大的开销,一个进程中包含 >= 1个线程(
进程是资源分配的最小单位
) - 一个进程会包含多个线程,每个线程间都有独享的空间和共享的内存,在java中独享的就是线程栈(每个线程都有一个线程栈),共享的就是堆内存、方法区等。(
线程是CPU调度的最小单位
)
以windows系统为例,看下图
进程
线程
如上图是我启动的一个java程序,最少会创建两个线程:一个用户线程(Main方法主线程)
、一个守护线程(GC垃圾回收线程)
并发和并行
- 并行:多个线程跑在多个CPU的操作系统上,这些线程是同时执行的,不需要进行CPU时间片的抢夺,不需要进行线程间的上下文切换,理解为:同一时间点,有多个任务同时在执行,互不干扰。
- 并发:线程数量较多时,CPU的数量是有限的,这些线程需要进行抢夺CPU时间片执行自己的任务,要进行线程间的上下文切换,同一时间点,有多个任务需要执行,但是只能一个一个执行,抢夺到CPU时间片的线程可以进行执行任务。
后面我们使用SpringBoot时可以整合线程池进行管理这些线程
Java线程的创建是依赖于系统内核的,通过JVM调用系统库创建内核线程,内核线程与Java线程是一对一的映射关系
线程的状态/阶段
-
创建
当一个线程对象被创建时,如:new Thread(),创建了一个对象,那么这个线程对象是创建状态
-
就绪
线程创建完毕后,调用对象的start()方法后,会在jvm中新开一个线程并且处于就绪状态,可以随时调用run()方法执行任务(在有CPU资源的时候,假如是多线程并发,那么需要抢夺到CPU时间片才能执行)
-
运行
就绪状态的线程获取了CPU,执行程序代码
-
阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。 阻塞的情况: 1.等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁) 2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中 3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
-
死亡
线程执行完了或者因异常退出了run()方法,该线程结束生命周期
线程的优先级
Java线程具有优先级,优先级高的线程可以有更多的机会获取到CPU时间片执行任务。
Java线程的优先级是用整数来表示,取值范围是 1~10
,在Thread类中有三个优先级的静态常量:
/**
* The minimum priority that a thread can have.
* 最低优先级 1
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
* 默认优先级 5
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
* 最高优先级 10
*/
public final static int MAX_PRIORITY = 10;
- Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
- 每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
- 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
如何创建一个线程
- 继承Thread类,重写run(),无返回值,不能抛出异常
- 实现Runnable接口,重写run(),无返回值,不能抛出异常
- 实现Callable接口,重写call(),有返回值,可放入
FutureTask
中阻塞获取返回值,可以抛出异常
为什么继承Thread类和实现Runnable接口重新的run()不能抛出异常,而call()方法可以呢,看源码
Thread类就是实现自Runnable接口,重写的run()方法
public
class Thread implements Runnable {
....
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
看下Runnable接口的run(),一个抽象方法,且没有抛出任何异常,所以子类不能抛出比父类更多、更大范围的异常
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
再看Callable的call(),是抛出一个Exception异常,所以子类重写时可以抛出范围 <= Exception 的异常。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Thread类的构造函数
构造方法名 | 备注 |
---|---|
Thread() | |
Thread(String name) | name为线程名字 |
Thread(Runnable target) | |
Thread(Runnable target, String name) | name为线程名字 |
第一二构造函数,适用于继承自Thread类,直接将任务和线程放在了一起,可扩性较低
第三四构造函数,适用于实现Runnable接口,这种是任务和线程分开放置,扩展性较高,并且方便实现资源对象共享
如果需要某些计算或需要线程执行完要获取返回值,可以用实现Callable接口方式,且放入到FutureTask
中,有get()
方法能阻塞获取返回值。
而且要注意,创建线程一定是Thread类对象的start()
方法,而不是调用run()方法,调用start()时,jvm会去创建一个线程然后再去调用对象的run()方法,如果直接调用run()方法,那只是进行了普通的方法调用,不会创建新的线程。
看几个简单的小例子
- 继承自Thread类,简单打印
public class MyThreadTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
// 启动线程
thread.start();
// 主线程执行打印
for(int i = 0; i < 100; i++){
System.out.println("主线程线程--->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
// 分支线程打印
for(int i = 0; i < 100; i++){
System.out.println("分支线程--->" + i);
}
}
}
- 实现Runnable接口,设置线程名称
public class MyRunnableTest {
public static void main(String[] args) {
MyRunnable rn = new MyRunnable();
Thread t = new Thread(rn, "s2");
t.start();
for(int i = 0; i < 100; i++){
System.out.println("主线程线程--->" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("分支线程" + Thread.currentThread().getName() + "--->" + i);
}
}
}
- 实现Callable接口,
可以有返回值,可以抛出异常,需要放入FutureTask中,可以阻塞当前线程获取返回值
。
public class MyCallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
// 创建线程,为就绪状态
new Thread(task).start();
// 阻塞主线程,获取返回值
Integer integer = task.get();
System.out.println(integer);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("call...被调用");
return 2022;
}
}
继承Thread和实现Runnable的区别
回忆下:一个线程启动肯定需要用到Thread类的start()方法,如果A类继承自Thread的(任务和线程都在一起),并且A类中进行了重写run(),那么我们直接创建一个A类,启动线程直接调用A类的start()方法即可。
而如果B类实现Runnable接口(只是一个任务类),重写run()方法,我们仍需要一个Thread类,调用Thread类对象的start()启动线程。
可以理解为:
- 继承自Thread类的A类,是一个任务和线程集一体的类
- 实现Runnable接口的B类,是一个任务类,这个任务需要一个线程去执行它
总结:实现Runnable接口具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 可以用线程池管理
(线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类)
常用方法
Thread类
方法名 | 解释 |
---|---|
static Thread currentThread() | 获取当前线程对象 |
String getName() | 获取线程对象名字 |
void setName(String name) | 修改线程对象名字 |
void sleep(long millis) | 当前线程休眠(毫秒),放弃CPU时间片的使用,让给其他线程,注意:sleep不会释放对象锁的。 |
void yield() | 暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程 |
join() | 在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态 |
interrupt() | 将会设置该线程的中断状态位,即设置为true,只是改变了中断状态,不会中断一个正在运行的线程 |
setPriority(int newPriority) | 设置线程优先级 |
int getPriority() | 获取线程的优先级 |
setDaemon(boolean on) | 设置是否为守护线程 |
… | … |
详解
yield()方法
暂停当前正在执行的线程,让此线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。
因此,yield()的目的是让具有相同优先级别的线程获取CPU资源,但是实际中无法保证yield()
百分百达到礼让目的,很可能当前线程刚回到就绪状态立刻又抢夺到了CPU的时间片,执行了任务。
sleep()方法
当前线程进入睡眠状态(阻塞状态),直至达到了设置的毫秒数时间后该线程进入就绪状态,可随时执行任务。而且在线程进入阻塞状态时,不会释放已经持有的对象锁
,来个例子
public class MyRunnableTest {
public static void main(String[] args) {
MyRunnable rn = new MyRunnable();
Thread t1 = new Thread(rn, "任务1");
Thread t2 = new Thread(rn, "任务2");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
private final Object o = new Object();
@SneakyThrows
@Override
public void run() {
synchronized (o) {
System.out.println("分支《" + Thread.currentThread().getName() + "》在执行,进入睡眠...");
Thread.sleep(5000);
System.out.println("分支《" + Thread.currentThread().getName() + "》睡眠结束,开始释放锁...");
}
}
}
看结果
分支《任务1》在执行,进入睡眠...
分支《任务1》睡眠结束,开始释放锁...
分支《任务2》在执行,进入睡眠...
分支《任务2》睡眠结束,开始释放锁...
任务1线程在进入阻塞状态时,对象锁并没有被释放掉,那么任务2只能也一起进入了阻塞状态,等待任务1将锁释放。
join()方法
强制将线程插入到当前线程中,当前线程进入阻塞,直至join()进来的线程执行完毕,来例子
public class MyRunnableTest {
public static void main(String[] args) throws InterruptedException {
MyRunnable rn = new MyRunnable();
Thread t1 = new Thread(rn, "任务1");
Thread t2 = new Thread(rn, "任务2");
t1.start();
t2.start();
// t1 t2强制加入
t1.join();
t2.join();
System.out.println("我是主线程");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程《" +Thread.currentThread().getName()+ "》执行" + i);
}
}
}
结果,主线程最后执行。
....
线程《任务2》执行89
线程《任务2》执行90
线程《任务2》执行91
线程《任务2》执行92
线程《任务2》执行93
线程《任务2》执行94
线程《任务2》执行95
线程《任务2》执行96
线程《任务2》执行97
线程《任务2》执行98
线程《任务2》执行99
我是主线程
interrupt()
将会设置该线程的中断状态位,即设置为true,线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。可以根据标记位,进行手动停止任务,例子如下
public class MyRunnableTest {
public static void main(String[] args) throws InterruptedException {
MyRunnable rn = new MyRunnable();
Thread t1 = new Thread(rn, "任务1");
t1.start();
System.out.println("我是主线程");
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行" + i);
}
t1.interrupt();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程《" +Thread.currentThread().getName()+ "》执行" + i);
if (Thread.interrupted()) {
break;
}
}
}
}
我是主线程
主线程执行0
线程《任务1》执行0
主线程执行1
线程《任务1》执行1
线程《任务1》执行2
主线程执行2
线程《任务1》执行3
主线程执行3
线程《任务1》执行4
线程《任务1》执行5
线程《任务1》执行6
主线程执行4
线程《任务1》执行7
线程《任务1》执行8
Object类
方法名 | 解释 |
---|---|
wait() | 使获取到此对象锁的线程进入无限休眠状态,且释放当前持有的对象锁,直至其他持有相同对象锁线程调用notify()或notifyAll()来唤醒此线程 |
notify() | 唤醒一个在这个对象的监视器上等待的单个线程 |
notifyAll() | 唤醒正在等待此对象监视器上的所有线程 |
详解
wait()方法、notify()
Obj.wait(),Obj.notify()必须要与synchronized(Obj)一起使用,其实就是wait()等方法必须要在synchronized代码块中使用,且代码块中的锁对象必须是当前次Obj对象,wait()与notify()都只能针对已经获取了Obj锁进行操作。
从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。
但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。
Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
来个案例,三个线程分别打印A,B,C各打印十次,需要使用wait()和notify()进行线程间的等待/唤醒交互。
public class MyABCTest {
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyABC printA = new MyABC("A", c, a);
MyABC printB = new MyABC("B", a, b);
MyABC printC = new MyABC("C", b, c);
// 确保顺序是按照 A,B,C执行
new Thread(printA).start();
Thread.sleep(300);
new Thread(printB).start();
Thread.sleep(300);
new Thread(printC).start();
}
}
class MyABC implements Runnable {
private String name;
private final Object prev;
private final Object self;
MyABC(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int i = 10;
while (i > 0) {
synchronized (prev) { // 现获取 上一个对象监视器(对象锁)
synchronized (self) { // 获取当前对象监视器(对象锁)
System.out.println(name);
i--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。
程序解读:
第一轮,为了确保程序执行是从 A->B->C开始的,我们需要先确保第一个执行的是线程A,A线程先执行时先拿到c对象和a对象的对象锁,MyABC("A", c, a)
其中c对象和a对象对于B、C线程都是被锁住的,那么B、C线程是执行不了的,因为B、C线程都需要c对象或a对象的锁
;A执行完毕后将a对象线程唤醒,将A线程c对象陷入无限等待,那么此时A线程已经无法执行了,除非被其他线程c对象唤醒。主程序睡眠0.3s后B线程开始执行,MyABC("B", a, b)
中拿到a对象和b对象的锁,B线程执行完毕后将b对象线程唤醒,B线程a对象陷入无限等待,此时B线程已经无法执行了,除非被其他线程a对象唤醒。主程序再次睡眠0.3s后B线程开始执行C线程,MyABC("C", b, c)
拿到b对象和c对象的锁开始执行任务,执行完毕后唤醒c对象陷入等待的线程,C线程b对象陷入无限等待。
注意:此时一波执行下来后,a对象和b对象都已经陷入无限等待了(也就是B线程和C线程现在已经执行不了了,只能等待被唤醒),然后c对象是被唤醒的,那么后面第二轮轮中A线程就可以先执行,A执行完再锁住A线程的c对象,唤醒a对象(A线程和C线程此时是陷入无限等待的),那么B线程就可以执行了…以此类推,完成分别打印A、B、C。
notifyAll()
notifyAll()与notify()的功能都是一样的,都是唤醒其他线程,然后不同的是notifyAll()是唤醒所有
在此对象上陷入无限等待的线程,而notify()是随机唤醒一个
。
sleep()和wait()区别
也算是一个比较常见的面试题了,大概区别就是:
相同点:
- 都是在多线程环境下,能阻塞线程
- wait()和sleep()都能通过
interrupt()
打断线程的暂停状态,使线程立刻抛出异常InterruptedException
不同点:
- sleep()源自Thread类,wait()源自Object类
- sleep()可以在线程的任意地方使用,wait()必须在同步方法或同步代码块中使用,wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
- sleep()睡眠时不会释放对象监视器(对象锁),仍然占用锁;而wait()进入睡眠时会释放锁
synchronized
作用域
synchronized是java中线程同步的关键字,作用域有两种:
- synchronized修饰普通实例方法,
作用于某个对象实例内
,同一个实例如果有两个synchronized方法,一个线程只要访问了其中一个,那么其他线程就不能同时再访问此实例中
的任何synchronized方法;而如果是不同实例,那么是互不影响的 - synchronized修饰静态方法,
作用于整个类
,一个类中只有一个类锁,防止多个线程同时访问这个类中的synchronized static 方法,可以对类的所有实例起作用。
synchronized对象锁
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
- synchronized关键字无论是放在方法上还是代码块中,它取得的锁都是对象,不是把一段代码或方法作为锁
- 一个对象只有一个锁,同理,一百个对象就有一百个锁
- 在能不加锁的情况下就别加,共享变量可以设置为局部变量或者别的解决方式
以下几种情况,锁对象的情况:
-
一个普通的同步方法
class A { public synchronized void aaa() { ... } }
那么它的锁对象是谁呢?它锁定的是调用这个同步方法的对象
A a = new A(); // 锁住的就是a对象 a.aaa();
-
synchronized代码块锁定this
class A { public void aaa() { synchronized(this) { } } }
此例子和上述一致,this锁的就是当前调用此方法的对象
A a = new A(); // 锁住的就是a对象 a.aaa();
-
synchronized代码块中锁定指定对象
class A { public Object a = new Object(); public void aaa() { synchronized(a) { ... } } }
这种就是在代码块中锁住指定的对象,只有获取到a对象的对象锁时才能执行代码块中的内容。
A a = new A(); // 锁的是A类中的a对象 a.aaa();
当场景中没有明确的对象作为锁时,只是为了达到让代码同步执行,可以创建一个特殊的变量来当做锁。
class Foo implements Runnable { private byte[] lock = new byte[0]; // 特殊的instance变量 public void methodA() { synchronized(lock) { } } }
总结
- 每一个对象都有一个锁,当一个线程获取到对象锁时,这个对象已经被第一个线程锁住了,其他线程是无法访问此对象中任意方法的(同步和非同步都不行)。
- 线程同步就是为了解决多线程间数据共享带来的数据安全性问题的。
- 当一个线程获得了对象锁,访问一个同步方法,假如同步方法中又调用了其他的同步方法(不是本对象的),那么这个线程拥有两把锁。
- 对于静态的同步方法,它锁住的是Class类对象(每个类只有一个Class类对象),非静态的方法,锁住的是当前的对象;静态同步方法和非静态同步方法的锁互不干扰。
- 必须要明确到底是要锁住哪个对象来达到同步手段。
线程安全问题
Java中的三大变量
- 实例变量:在堆中。
- 静态变量:在方法区中。
- 局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
-
因为局部变量不共享。(一个线程一个栈。)
-
局部变量在栈中。所以局部变量永远都不会共享。
-
实例变量在堆中,堆只有1个。
-
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
如何解决线程安全问题
只要是涉及到加锁的,都是通过时间来换取安全性,不到万不得已不要加锁。
- 尽量使用
局部变量
,局部变量没有线程安全问题 - 实例变量的话,考虑多创建对象,不同的对象引用不同,那么变量间都是互不干扰的
- 采用锁机制
死锁
死锁其实就是在多线程环境下,发生资源抢夺,双方都不愿意释放锁,一直僵持着。
面试:死锁代码,两线程发生资源抢夺。
public class DeadLockTest {
public static void main(String[] args) {
Car car = new Car();
Bike bike = new Bike();
Thread carT = new Thread(new LockCar(car, bike));
Thread bikeT = new Thread(new LockBike(car, bike));
carT.start();
bikeT.start();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class LockCar implements Runnable {
private Car car;
private Bike bike;
@SneakyThrows
@Override
public void run() {
synchronized (car) {
System.out.println("LockCar拿到car了");
Thread.sleep(1000);
synchronized (bike) {
System.out.println("LockCar拿到bike了");
}
}
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class LockBike implements Runnable {
private Car car;
private Bike bike;
@SneakyThrows
@Override
public void run() {
synchronized (bike) {
System.out.println("LockBike拿到bike了");
Thread.sleep(1000);
synchronized (car) {
System.out.println("LockBike拿到car了");
}
}
}
}
class Car {
}
class Bike {
}
如何避免死锁
- 按照顺序加锁,每个线程都按照相同的顺序加锁不会造成死锁,多个线程先去抢夺同一个资源,再去抢夺其他。
- 给锁加时限,如果超时那就放弃获取锁。
- 按照线程间获取锁的关系检查是否会死锁,如果发生死锁执行一定的回滚策略,如中断线程。