线程
学习线程,我们大致要了解这样一些东西
线程概念
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
就像是去银行取钱,如果只有一个窗口张三在办业务,所有人都在等这一个窗口,这样就有点麻烦,但是,张三又叫了李四,王五两个同事又开了两个窗口来干一样的事,都是在帮银行办事,这样的流水线就叫多线程,(张三喊的李四和王五,我们一般把张三成为主线程).
进程线程区别
首先,我们要知道,为啥要有进程?因为我们的系统是支持多任务的,程序员也就需要"并发编程".
什么是并发编程?(首先我们谈谈并行和并发:并行:微观上,两个cpu核心,同时执行两个任务代码.并发:微观上,一个cpu核心,先执行一会任务一,在执行一会任务二,在任务三…在任务一…这样他只要切换的够快,从宏观上来看,他就像是在同时执行多个任务)所以,并发和并行这两件事,只在微观上区分,宏观上不区分,所以我们常常用并发来指代并行+并发.)
这里谈到进程和线程区别:
1:
进程包含线程,一个进程里可以有一个或多个线程
2:
进程和线程都是为了处理并发编程这样的场景.但进程频繁创建销毁时候效率低,相比之下,线程更轻量,效率高(因为少了申请释放资源的过程)
3:
操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位.操作系统创建线程,是要在cpu上调度执行,线程是操作系统调度执行的基本单位
4:
进程具有独立性,一个进程挂了不影响其他进程,同一个进程中的多个线程,公用一个内存空间,一个线程挂了,可能影响其他线程,甚至让整个进程崩溃.
(举个例子):(如果把进程比喻成一个工厂,现在要生产1w部手机:
两个方案:1:搞两个工厂,一人5k
2:一个工厂:搞2个生产线,在分别生产5k
可能时间差不多,但里面的成本可想而知)
java创建线程
在Java中使用 Thread 类这个类的对象来表示操作系统中的线程
五种方法:
1:继承 Thread, 重写 run
class MyThred extends Thread{
@Override
public void run(){
System.out.println("hello thread");
}
}
public class Demo1{
public static void main(String[] args) {
Thread thread = new MyThred();
thread.start();
}
}
第二种:实现 Runnable, 重写 run
class MyRunnable implements Runnable{
public void run(){
System.out.println("hello");
}
}
public class Demo3
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
第三种:继承 Thread, 重写 run, 使用匿名内部类
public class Demo4{
public static void main(String[] args) {
Thread thread = new Thread(){
public void run(){
System.out.println("hello world");
}
};
thread.start();
}
}
第四种:实现 Runnable, 重写 run, 使用匿名内部类
public class Demo5{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
});
thread.start();
}
}
第五种:使用 lambda 表达式
public class Demo6{
public static void main(String[] args) {
Thread thread = new Thread(() ->{
System.out.println("hello thread");
});
thread.start();
}
}
Thread的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明
中断线程
自己定义一个变量来
public class Demo10 {
private static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!isQuit) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// 只要把这个 isQuit 设为 true, 此时这个循环就退出了, 进一步的 run 就执行完了, 再进一步就是线程执行结束了.
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit = true;
System.out.println("终止 t 线程!");
}
}
但是这个做法并不严谨,更好的做法是:
使用Thread中内置的一个标志位,来进行判定,可以通过:
Thread.interrupted() 这个是静态方法
Thread.currentThread().isInterrupted() 这个是实例方法
public class Demo11 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 当触发异常之后, 立即就退出循环~
System.out.println("这是收尾工作");
break;
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在主线程中, 调用 interrupt 方法, 来中断这个线程.
// t.interrupt 的意思就是让 t 线程被中断!!
t.interrupt();
}
}
线程等待
线程和线程之间,调度顺序是完全不确定的…如果想要线程的顺序可控,线程等待就是一种方法.
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);//1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// 在主线程中就可以使用一个等待操作. 来等待 t 线程执行结束.
try {
t.join(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
获取当前线程的引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class Demo13 {
public static void main(String[] args) {
// Thread t = new Thread() {
// @Override
// public void run() {
// // System.out.println(Thread.currentThread().getName());
// System.out.println(this.getName());
// }
// };
// t.start();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// System.out.println(this.getName());
System.out.println(Thread.currentThread().getName());
}
});
t.start();
// 这个操作是在 main 线程中调用的. 因此拿到的就是 main 这个线程的实例
System.out.println(Thread.currentThread().getName());
}
}
线程休眠
休眠当前线程
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
这个Sleep这个方法,本质上就是把线程PCB从就绪队列,移动到阻塞队列.
注: 当线程调用 sleep / join / wait / 等待锁 … 就会把 PCB放到另一个队列(阻塞队列)
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
线程状态
NEW: 安排了工作, 还未开始行动
把Thread对象创建好了,但是没有调用start~
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
就绪状态:
处于这个状态的线程,就是在就绪队列中,随时可以调度到CPU上~
如果代码没有进行sleep,也没有进行其他的可能导致阻塞的操作,代码大概率是处在Runnable状态~
BLOCKED: 这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞(阻塞状态之一) synchronized~~
WAITING: 这几个都表示排队等着其他事情
当前线程正在等待唤醒,导致了阻塞(阻塞状态之一) wait~~
TIMED_WAITING: 这几个都表示排队等着其他事情
代码中调用了sleep,就会进入到TIMED_WAITING
join(超时)
意思就是当前的线程在一定时间=内,是阻塞状态
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
// 这里啥都不能有!!!
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
一定时间之后,阻塞状态解除,这种情况就是TIMED_WAITING…也是属于阻塞的状态之一
TERMINATED: 工作完成了.
操作系统中的线程已经执行完毕,销毁了,但是Thread对象还在,获取到的状态
线程安全
看下面这段代码:
class Counter {
// 这个 变量 就是两个线程要去自增的变量
volatile public int count;
public void increase() {
count++;
}
synchronized public static void func() {
}
}
public class Demo15 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
// 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();
// 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
System.out.println(counter.count);
}
}
我们想得到的是10w,我运行了四次,结果每次结果都不一样
为啥会这样,下面来分析一下:
那怎样做到线程安全,来举个例子:
就像这样,总不能一窝蜂的去一起上同一个厕所吧,像这样把门锁上,一个一个来,不就安全了吗!
我们给某一个线程上锁,另一个线程就等着,结束在解锁
import java.util.Scanner;
public class Demo16 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("循环结束! t 线程退出!");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个 isQuit 的值: ");
isQuit = scanner.nextInt();
System.out.println("main 线程执行完毕!");
}
}
简单总结一下线程不安全的原因
1:
线程都是抢占式执行,线程间的调度充满随机性.[万恶之源,无可奈何]
2:
多个线程对同一个变量进行修改操作~~
3:
针对变量的操作不是原子(计算机中说原子都是说不能再分的了)
针对这些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的
通过加锁操作,也就是把好几个指令打包成一个原子的了~
加锁操作,就是把这里的操作打包成一个原子操作
4:
内存可见性,也会影响线程安全
5:
指令重排序,也会影响到线程安全问题~~
synchronized
特性
1:互斥: synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
2:刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性.
3:可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
(啥叫重入锁,直观的讲,同一个线程针对同一把锁,连续加锁两次,如果出现了死锁,就是不可重入锁,如果不会,就是可重入锁)(死锁:简单点举个例子:我喜欢蘸酱油吃饺子,我姐喜欢蘸醋吃饺子,一天,我拿着一瓶醋想蘸酱油,我姐拿着一瓶酱油想蘸醋,我说:“你给我酱油我就给你醋”,我姐说:“你给我醋我就给你酱油”.大家都这样僵着了,谁也不让谁!)
死锁的四个必要条件:
1:互斥使用:一个锁被一个线程占用之后,其他线程占用不了(锁的本质,保证原子性)
2:不可抢占:一个锁被一个线程占用以后,其他线程不能把这个锁抢走(解释一下锁竞争(所对象):一个女神有俩人在追,其中一个追到手了,另一个就只有等分手了才能去重新追)
3:请求和保持:当一个线程占据了多把锁之后,除非显示的释放锁,否者这些锁都是被线程持有的~
4:环路等待:等待关系,成环了~~A等B,B等C,C等A;(参考哲学家问题)
volatile 关键字
volatile 能保证内存可见性
代码在写入 volatile 修饰的变量的时候,
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
看一个代码:
public class Demo1 {
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
此时可以看到, 最终 count 的值仍然无法保证是 100000
通俗点说:计算机想要执行一些计算,就需要把内存的数据读到cpu寄存器中,然后再里面计算再返回到内存
然而cpu访问寄存器的速度,比访问内存快太多了,当他多次访问内存发现,都一样?~~那就偷个懒吧~从而漏加了一些.
wait 和notify
等待和通知~~
wait 和 notify都是Object对象的方法
调用wait方法的线程就会阻塞,要有其他线程通过notify方法来通知
public class Demo18 {
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 进行 wait
synchronized (locker) {
System.out.println("wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 之后");
}
});
t1.start();
Thread.sleep(3000);
Thread t2 = new Thread(() -> {
// 进行 notify
synchronized (locker) {
System.out.println("notify 之前");
locker.notify();
System.out.println("notify 之后");
}
});
t2.start();
}
}
wait内部会做三件事~
1:先释放锁
2:等待其他线程通知
3:收到通知后,重新获取锁,并继续向下执行
所以,要使用wait/notify就得搭配synchronized
wait哪个对象,就得针对哪个对象加锁