文章目录
一、认识线程
1、什么是线程?
一个线程就是一个 “ 执行流 ”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间 “同时” 执行着多份代码,从而能更加高效的处理业务,从而解决并发编程的问题。
2、线程 VS 进程
- 进程是包含线程的,每个进程至少有一个线程存在,即主线程main。
- 进程和线程都是为了实现并发编程
- 进程具有独立性, 各个进程之间相互不影响, 但是如果一个线程挂了, 那么可能会影响到同一个进程中的其他线程, 甚至使得整个进程崩溃
- CUP将资源分配给每个进程,各个进程之间不共享资源。 而同一个进程里的各个线程共享同这块分配的内存空间。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
二、Thread类
Thread 类是 JVM 用来管理线程的一个类,每个线程都有一个唯一的 Thread 对象与之关联。
1、线程的创建
1、通过创建一个继承 Thread类的线程类类创建线程对象。
// 子类实现 Thread类
class MyThread extends Thread {
@Override
public void run() {
// run方法中的逻辑,是在新创建出来的线程中,被执行的代码
System.out.println("hello thread");
}
}
public class TestDemo1 {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new MyThread();
// 开启线程
t1.start();
}
}
简化写法:使用匿名内部类
public class TestDemo1 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello");
}
};
t.start();
}
}
2、子类实现 Runnable接口
//子类实现 Runnable接口, Runnable 就是在描述一个任务
class MyThread3 implements Runnable {
@Override
public void run() {
// run方法中的逻辑,是在新创建出来的线程中,被执行的代码
System.out.println("hello thread!");
}
}
}
public class TestDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread3());
t1.start();
}
}
简化写法:使用匿名内部类
public class TestDemo3 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
}
只有调用start()方法,系统才会开启一个线程。run()方法只是执行了一个方法。 这里的run() 方法描述了这个线程的任务。
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
2、线程的终止interrupt()
- 设置标志位,让线程退出循环
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (flag) {
}
});
t.start();
Thread.sleep(1000);
// 等待1s后,设置标志位,让 t 线程终止
flag = false;
System.out.println("主线程终止");
}
- 调用interrupt(),通知线程终止
// interrupt()标记线程是否被中断
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread");
try {
// 这个线程绝大部分都在休眠状态
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 触发异常后,立即退出循环
break;
}
}
});
t.start();
Thread.sleep(5000);
// 让t线程终止 ----- 就会更改状态 ---> 就绪态-终止
// 如果位于阻塞态(睡眠) --- 就会抛异常
t.interrupt();
}
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程(阻塞 — 抛异常) (就绪态/运行态 — 设置标志位) |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
3、线程等待 join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,先买到票,才能去看电影,这时我们需要一个方法明确等待线程的结束 join() 。也可以指定等待的时间(但是并不会出现死等的情况,等不到就不会继续等了)。
4、线程状态
NEW :线程对象已经创建好了,还没有调用start()方法
RUNNABLE : 就绪态,处在就绪队列中,随时可以被调度。
TERMINATED : 线程已经将run()方法执行完毕,销毁了,但线程对象还在。
三种阻塞状态:
BLOCKED : 当前状态在等待锁🔒,synchronized。
WAITING : 当前线程在等待被唤醒😴,调用了wait()方法。
TIMED_WAITING : 系统中调用了sleep()、join() 等方法。线程在一定时间内,处于阻塞状态。
5、线程安全问题
由于操作系统调度线程的随机性,就会导致程序的运行结果跟想象的可能不一样。
如果因为这样的随机性导致有了bug,那么就认为代码是线程不安全的。
如果调度的随机性没有带来bug,那么代码就是线程安全的。
例如让两个线程分别使一个变量sum=0自增5_0000次,我们知道如果不出差错的话,结果应该是10_0000)
public static int sum;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
// t1线程使sum自增
sum++;
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
// t2线程使sum自增
sum++;
}
});
t2.start();
t1.join();
t2.join();// 分别等t1,t2线程结束后,统计sum的值
System.out.println(sum);
}
对于一个简单的sum++操作,实际上是执行了三条cup指令:
1、将内存中的sum的值,加载到cup寄存器(load)
2、把寄存器的值 +1(add)
3、将寄存器的值写回内存(save)
正是因为操作系统调度的随机性,导致两个线程同时执行这三条指令时,两个线程的顺序具有随机性,假设某一次自增的执行顺序如下
可以看到这时两个线程分别执行了sum++操作,但是sum却只增值了一次,而大部分情况下都是这样乱序的状态,只有两个线程串行执行的时候才会得到正确的结果,所以导致sum的值与理想的相差很大,这也是操作系统在调度的时候的随机性,从而引发了线程安全问题。
产生线程不安全的原因:
-
线程是抢占式执行的,线程之间的调度充满随机性
-
多个线程对一个变量进行修改(如果多个线程针对不同的对象修改、如果多个线程同时读一个变量都是线程安全的)
-
针对变量不是 原子性 的。 加锁操作(synchronized )就是将操作变成原子的。
-
内存可见性 导致线程安全问题,volatile关键字可以保证内存可见性。
-
指令重排序 导致线程安全问题(这也是编译器做出的优化),大部分情况下指令重排序是没问题的。synchronized
指令重排序 — 假设我们需要去超市买东西(按照清单的路线)
编译器会智能调整执行顺序以提高效率(做出优化):
三、synchronized 关键字
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象synchronized 就会阻塞等待。 直到占用锁的线程把锁释放为止。
- 进入 synchronized 修饰的代码块,相当于 加锁
- 退出 synchronized 修饰的代码块,相当于 解锁
使用synchronized,本质上是在针对某个“对象”进行加锁,要执行synchronized中的代码块,就先要拿到该锁对象
synchronized加锁方式:
- 直接修饰普通方法( 相当于针对this加锁 )
- 修饰一个代码块( 这里可以指定任意对象 )
- 修饰静态方法( 相当于针对当前类的类对象加锁 — 类名.class )
四、volatile 关键字
计算机想要执行一些计算,就需要把数据读到寄存器中,然后在寄存器中计算,再写回到内存中,而cup访问寄存器的速度比直接访问内存的速度快很多。所以当连续多次访问内存,发现结果都一样,就会去寄存器读数据。这就是 内存可见性 问题。
运行结果:
加上volatile关键字:
五、wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。就需要使用到 wait(等待) 和 notify(通知)的方式来控制执行的顺序。
-
当一个线程调用了 wait() 方法后,该线程就会进入阻塞状态。
- 当调用了 wait() 操作后,会做三件事:
- 先释放锁
- 等待其他线程的通知
- 收到通知后,重新获取锁,继续执行
- 所以要调用 wait() 方法,就必须持有锁,和 synchronized 搭配使用。
-
其他线程来调用 notify() 方法来通知该阻塞态的线程继续执行下去。
例题:有三个线程,线程名称分别为:a,b,c。每个线程打印自己的名称。需要让他们同时启动,并按 c,b,a 的顺序打印。
因为一共有三个同时工作的线程,我们就需要两个锁对象来控制顺序 ( o1 和 o2 )
public class Main {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread a = new Thread(() -> {
synchronized (o1) {
try {
o1.wait();
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程a");
Thread b = new Thread(() -> {
synchronized (o2) {
try {
o2.wait();
System.out.println(Thread.currentThread().getName());
synchronized (o1) {
o1.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程b");
Thread c = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
synchronized (o2){
o2.notify();
}
},"线程c");
a.start();
b.start();
c.start();
}
}
运行结果: