文章目录
- 一、线程基础知识
- 1.1 进程是什么
- 1.2 线程是什么
- 1.3为什么使用线程
- 1.4 线程的生命周期
- 二、线程基础操作
- 2.1 新建线程
- 2.1.1 如何创建线程
- 2.1.2 调用start()和调用run()的区别
- 2.2 终止线程
- 2.2.1 stop()方法
- 2.2.2 为什么不推荐使用stop()方法
- 2.3 线程中断
- 2.3.1 线程中断
- 2.3.2 与线程中断有关的方法
- 2.3.3 sleep()方法
- 2.3.4 sleep()由于中断而导致的问题
- 2.4 等待(wait)和通知(notify)
- 2.4.1 wait()、notify()和notifyAll()工作原理
- 2.4.2 wait()方法的使用限制
- 2.4.3 wait()和sleep()区别
- 2.5 挂起(suspend)和继续执行(resume)
- 为什么不推荐使用suspend()
- 2.6 等待结束(join)和谦让(yield)
- 2.6.1 join方法
- 2.6.2 join方法的本质
- 2.6.3 yield方法
- 三、volatile与Java内存模型(JMM)
- 四、守护线程
- 4.1 守护线程
- 4.2 用户线程
- 4.3 何时设置守护线程
- 五、线程安全与Synchronized
- 5.1 volatile的问题
- 5.2 synchronized的作用
- 5.3 synchronized的用法
一、线程基础知识
1.1 进程是什么
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
1.2 线程是什么
线程就是轻量级的进程,是程序执行的最小单位
1.3为什么使用线程
线程间的切换和调度成本远远小于进程
1.4 线程的生命周期
- 线程的相关状态在Thread类中的State枚举中定义
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
注意:从
NEW
状态出发后,线程不能再回到NEW
状态;同理,处于TERMINATED
状态的线程也不能再回到RUNNABLE
状态
二、线程基础操作
2.1 新建线程
2.1.1 如何创建线程
- 继承
Thread
类 - 实现
Runnable
接口 - 实现
Callable
接口 - 使用线程池
2.1.2 调用start()和调用run()的区别
- 调用
start()
:会新建一个新的线程,用这个新的线程执行这个run()
方法 -
run()
:就是普通的方法调用,不会创建新线程
2.2 终止线程
2.2.1 stop()方法
JDK 的 Thread
类提供了一个 stop()
方法,使用它可以立即停止一个线程。但注意它是一个被标注为废弃的方法
2.2.2 为什么不推荐使用stop()方法
-
stop()
方法会强行终止线程,如果把执行到一半的线程终止,可能会引起一些数据不一致的问题 stop()
方法在强行终止线程时,会立即释放这个线程所持有的锁,而这些锁或许恰恰是用来维持对象一致性的。如果线程写到一半,使用stop()
将其强行终止,那么对象就会被写坏,同时由于锁被释放,另一个等待该锁的线程就会读到这个不一致的对象。
写线程获取锁,读线程等待,写线程本来想将ID和Name改为1,在将ID改为1后,由于使用了
stop()
方法,写线程被强行终止并被强行释放了锁,但此时Name还未写入;写线程释放锁之后,读线程就可以获取锁了,但是因为写线程写数据时并没有写入完整的数据,导致读线程读取的数据是有问题的(本来读取ID=1,Name=1;但现在读取的是ID=1,Name=0)
2.3 线程中断
2.3.1 线程中断
线程中断就是给线程发送一个通知,告知目标线程希望其退出。线程接收到通知后如何处理,有线程自己决定
2.3.2 与线程中断有关的方法
// 1、中断线程:这是一个实例方法
public void Thread.interrrupt()
// 2、判断线程是否被中断
public boolean Thread.isInterrupted()
// 3、判断线程是否被中断,并清除当前中断状态
public static boolean Thread.interrupted()
-
Thread.interrrupt()
:这是一个实例方法,它通知目标线程中断,即设置中断标志位。(中断标志位表示当前线程已被中断) -
Thread.isInterrupted()
:这是一个实例方法,它通过检查中断标志位来判断目标线程是否被中断 -
Thread.interrupted()
:这是一个静态方法,它可以判断当前线程的中断状态,但同时会清除当前线程的中断标志位
2.3.3 sleep()方法
public static native void sleep(long millis) throws InterruptedException;
-
Thread.sleep()
方法会让当前线程睡眠一段时间。当线程睡眠时,如果被中断,会抛出一个InterruptedException
异常,这不是运行时异常,所以程序必须捕获并处理它。
2.3.4 sleep()由于中断而导致的问题
Thread.sleep()
方法由于中断而抛出异常时会清除中断标记。如果不对其进行处理,那么在下一次循环时,无法捕获这个中断。所以在异常处理中,要再次设置中断标记。
2.4 等待(wait)和通知(notify)
2.4.1 wait()、notify()和notifyAll()工作原理
注意:
wait()
方法和notify()
方法在Object
类中,而不是Thread
类中,所以任何对象都可以调用这个方法。notify()
方法选择线程是随机的,所以是不公平的
- 如果一个线程调用了
object.wait()
方法,那么它就会进入object
对象的等待队列(因为系统运行多个线程时可能会同时等待一个对象,所以这个等待队列可能会有多个线程)。 - 当
object.notify()
方法被调用时,对象就会从等待队列中随机选择一个线程,将其唤醒。 - 当
object.notifyAll()
方法被调用时,对象就会将等待队列中所有线程唤醒。
2.4.2 wait()方法的使用限制
必须要在对应的 synchronized
语句中
2.4.3 wait()和sleep()区别
- 相同点:
wait()
和sleep()
都可以让线程等待若干时间 - 不同点:
-
wait()
方法可以被唤醒 -
wait()
方法释放锁,而Thread.sleep()
方法不释放任何资源(包括锁)
2.5 挂起(suspend)和继续执行(resume)
线程挂起(suspend)和继续执行(resume)是一对相反的操作,被挂起的线程,必须要等到
resume()
方法操作后,才能继续执行。
但这两个方法已被标注为废弃的方法,不推荐使用
为什么不推荐使用suspend()
suspend()
在挂起线程的同时,线程不会释放任何锁资源。所以任何需要访问此锁的线程,都无法继续运行。只有对应线程上执行了 resume()
方法,被挂起的线程才可以继续执行,从而其它需要访问此锁的线程才能继续运行。
- 若
resume()
方法在suspend()方法前就执行了,那么被挂起的线程很难有机会继续执行。 - 更严重的是,此线程被挂起后不释放锁,因此可能会导致整个系统工作不正常
2.6 等待结束(join)和谦让(yield)
2.6.1 join方法
有时候,一个线程的继续执行,可能依赖于另一个/多个线程的输出。此时,这个线程就需要等待依赖线程全部执行完毕,才能继续执行
-
public final void join() throws InterruptedException
:表示无限等待,他会一直阻塞当前线程,直到目标线程执行完毕 -
public final synchronized void join(long millis) throws InterruptedException
:设定一个最大等待时间,如果超过给定的时间目标线程仍未执行完毕,当前线程不会等待目标线程执行完毕,而是会直接继续运行
2.6.2 join方法的本质
-
join()
方法的本质是让调用线程wait()方法在当前线程对象实例上 - 下面是
JDK
中join()
方法的核心代码
public final synchronized void join(long millis)
while (isAlive()) {
wait(0);
}
}
它让调用线程在当前线程对象上进行等待。当线程执行完毕后,被等待的线程会在退出前调用
notifyAll()
方法通知所有等待的线程继续执行。
2.6.3 yield方法
-
public static native void yield()
:它会使当前线程让出CPU
注意:
让出CPU不代表当前线程不执行。当前线程让出CPU后,还会继续争夺CPU,至于能否抢到,随缘。
三、volatile与Java内存模型(JMM)
-
volatile
对保证操作的原子性有很大帮助,但它不能代替锁,因为它无法保证一些复合操作的原子性,例如:i++
public class volatileTest {
// 计数器
private static volatile int i = 0;
/**
* 自增任务
*/
public static class PlusTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int j = 0; j < 10; j++) {
ts[j] = new Thread(new PlusTask());
ts[j].start();
}
for (int j = 0; j < 10; j++) {
ts[j].join();
}
System.out.println(i);
}
}
- 如果
i++
是原子性的,那么输出结果应该是100000 - 但是多个线程同时对i进行写入时,其中一个线程的结果可能会覆盖另一个的,所以实际上每次输出的值都会小于100000
四、守护线程
4.1 守护线程
- 守护线程是一种特殊的线程,在后台完成一些系统性的服务,比如:垃圾回收线程、JIT线程等
4.2 用户线程
- 用户线程可以认为是系统的工作线程,他会完成相关的业务操作。
当Java应用内只有守护线程时,Java虚拟机就会退出
原因:如果用户线程全部结束,就意味着程序无事可做了;守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束了
4.3 何时设置守护线程
- 设置守护线程必须要在线程
start()
之前设置,否则设置守护线程会失败并且会得到一个异常。但是线程会被当作用户线程继续执行。
五、线程安全与Synchronized
5.1 volatile的问题
-
volatile
不能真正保证线程安全。它只能确保一个线程修改了数据后,其它线程能够看到这个改动;若多个线程同时修改同一个数据,依然会产生冲突
public class volatileTest {
// 计数器
private static volatile int i = 0;
/**
* 自增任务
*/
public static class PlusTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int j = 0; j < 10; j++) {
ts[j] = new Thread(new PlusTask());
ts[j].start();
}
for (int j = 0; j < 10; j++) {
ts[j].join();
}
System.out.println(i);
}
}
- 如果i++是原子性的,那么输出结果应该是100000
- 但是多个线程同时对i进行写入时,其中一个线程的结果可能会覆盖另一个的,所以实际上每次输出的值都会小于100000
5.2 synchronized的作用
- 关键字
synchronized
的作用是线程间的同步。对需要同步的代码进行加锁,使得每一次只有一个线程进入同步代码块,从而保证线程间的安全性
5.3 synchronized的用法
- 指定加锁对象:对给定对象加锁,进入同步代码块前要获得给定对象的锁
- 直接作用于实例方法:相当于给当前对象进行加锁,进入同步代码块前要获得当前对象的锁
- 直接作用于静态方法:相当于给当前类对象进行加锁,进入同步代码块前要获得当前类对象的锁