一、什么是线程
上篇引入了进程这个概念,最主要的目的是为了解决“并发编程”这样的问题,确实,多进程编程已经可以解决并发编程的问题了,已经可以利用起来cpu多核资源了。但是,进程太重了(这里的重,主要体现在“资源分配/回收”上),创建一个进程,销毁一个进程,调度一个进程,开销都比较大。此时,线程应运而生,线程也叫作“轻量级进程”。 在解决并发编程问题的前提下,想让创建、销毁。调度的速度更快一些,线程就起到了至关重要的作用。
线程为什么更“轻量”? 是因为把多次申请资源/释放资源的操作给省下了。
二、线程和进程之间的区别与联系
一个进程中可以有一个或多个线程,同一个进程里的多个线程之间共用乐进程的同一份资源(主要指内存和文件描述符表)
# 内存: 比如线程1 new的对象,在线程2、3、4里都可以直接使用
# 文件描述符表: 比如线程1打开的文件,在线程在线程2、3、4里都可以直接使用
线程和进程之间到底是什么关系呢?这里给大家举个例子,就可以很好的明白了
假如你是开工厂的,起初规模比较小,你的工厂里只有一条生产线,此时原材料运到你的工厂,你加工好之后运走,一切顺利运行。如下图
![](https://i-blog.csdnimg.cn/blog_migrate/2ea4473e3c7afb87927d09abd4946ea0.png)
某一天,为了把生产力扩大,有两种方案:
第一种方案:此时你在工厂旁边买了一块地,又建了一个工厂,开了一条生产线,重新搞物流体系。这就相当于多进程版本的方案,如下图
![](https://i-blog.csdnimg.cn/blog_migrate/3f12897acda382ec64e618aad63dfc39.png)
第二种方案:在工厂中再搞一套生产线,场地和物流体系都可以复用之前的,只是搞第一套生产线的时候,需要把资源申请到尾,后续再加新的生产线,复用之前的资源即可
这就是多线程版本的方案,如下图
![](https://i-blog.csdnimg.cn/blog_migrate/7f0cf018fb5cef577ad7c42753a1b311.png)
这就说明了引入线程这个概念的必要性。操作系统实际调度的时候就是以线程为单位进行调度的
三、线程创建的五种写法
继承Thread,重写run方法
class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello Thread");
}
}
实现Runnable接口,重写run方法
# Runnable作用,是描述一个“要执行的任务,“ run方法就是任务的执行细节
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello world");
}
}
使用匿名内部类,继承Thread
public static void main(String[] args) {
Thread t=new Thread(){ //匿名内部类: 创建了一个Thread的子类,(该子类没有名字,所以叫“匿名”)
@Override
public void run() {
System.out.println("hello");;
}
};
}
创建了一个Thread的子类 (这个子类没有名字),所以叫“匿名”
创建了子类的实例,并且让t引用指向该实例
使用匿名内部类,实现Runnable
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
这个写法和第2中是同一作用
使用Lambda表达式 最简单,推荐写法~
public static void main(String[] args) {
Thread thread=new Thread(()->{
System.out.println("hello thread");
});
}
把线程要做的任务用lambda表达式来描述,直接把lambda传给Thread的构造方法
四、Thread类中 run和start的区别
Start是真正创建了一个线程(从系统这里创建的),并且让新线程调用run。线程是独立地执行流。
Run只是描述了线程要干的活是啥。如果直接在main中调用run,此时没有创建想新线程,全是main线程一个线程在干活
五、总结Thread类的基本用法
Thread的常见构造方法:
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明
-start() 启动一个线程
调用start方法,才真正的在操作系统的底层创建出一个线程。
中断一个线程- 调用interrupt()方法来通知
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
thread.start();
Thread.sleep(3000);
thread.interrupt();
}
Thread.currentThread() ->获取当前线程对象
is Interrupted() ->判断当前线程是否存活
在main线程中调用thread.interrupt(),可通知线程thread中断。
注意:此时只是通知线程thread该中断了, 但此线程是否真正中断具体情况具体分析.
thread 收到通知的方式有两种:
1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以InterruptedException 异常的形式通知,清除中断标志当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
2. 否则,只是内部的一个中断标志被设置,thread 可以通过
Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
等待一个进程-join()
public static void main1(String[] args) {
Thread thread=new Thread(()->{
for (int i = 0; i <3 ; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
thread.join(); //让main线程等待thread线程结束,(等待t的run执行完)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
大家可以试试人如果把join注释掉,会是什么现象?
附录:
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis,int nanos) | 同理,但可更高精度 |
获取当前线程的引用-
这个方法前面提到过。
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
休眠当前线程-sleep()
也是比较熟悉的一组方法,切记,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
方法 | 说明 |
public static void sleep(long millis)throws InterruptedException | 休眠当前线程millis毫秒 |
public static void sleep(long millis,int nanos)throws InterruptedException | 可以更高精度的休眠 |
六、总结 Java 线程的几种状态
NEW 创建了Thread对象,但是还没调用start(此时内核里还没创建对应的PCB)
TERMINATED 表示内核中的PCB已经执行完毕了,但是Thread对象还在
RUNNABLE 可运行的
正在CPU上执行的
在就绪队列里,随时可以去CPU上执行的
WAITING
TIMED_WAITING
BLOCKED
状态之间的切换条件如图:
![](https://i-blog.csdnimg.cn/blog_migrate/87d16781cb0ca3847f961c8d5e61d4ae.png)
七、线程安全问题的原因和解决方案(重点)
多线程带来的风险-线程安全问题,造成线程不安全有以下几种原因:
根本原因:线程的抢占式执行,随机调度。此原因我们没办法解决,无能为力。
修改共享数据
当多个线程针对一个变量进行修改时,往往得到的结果与我们预期不同。
代码1:
class Counter1{
public int count=0;
public void add(){
count++;
}
}
public static void main(String[] args) { //两个线程修改共享资源 : 线程不安全
Counter1 counter=new Counter1();
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
counter.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
counter.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count="+counter.count); //count 不等于100000, 这就是线程不安全
}
运行此代码,预期结果count=100000,但实际结果并不等于100000。
原子性
其实对一个变量进行修改由三步操作组成的:
1)从内存把数据读到CPU
2)进行数据更新
3)把数据歇会CPU
如果不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量进行操作,中途其他线程插入进来了,如果这个 操作被打断了,结果就可能是错误的
内存可见性问题
如果一个线程读,一个线程改,也可能出问题
代码2:
import java.util.Scanner;
class MyCounter{
public int flag=0;
}
public static void main(String[] args) {
MyCounter myCounter=new MyCounter();
Thread t1=new Thread(()->{
while (myCounter.flag==0){
//循环体空着
}
System.out.println("t1循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入一个整数:");
myCounter.flag=scanner.nextInt();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
预期:t2把flag改为非0的值之后,t1随之就结束 实际: t1未结束 flag变量加上volatile后,t1可以随之结束,
内存可见性问题,其他一些资料谈到JMM(Java Memory Model)java内存模型,从JMM的角度重新表述内存可见性问题:
java程序里,主内存,每个线程还有自己的工作内存,t1线程进行读取的时候,只是读取了工作内存的值,t2线程进行修改的时候,先修改的自己的工作内存的值,然后再把工作内存的内容同步到主内存中。,但是由于编译器优化,导致t1没有重新的从主内存同步数据到工作内存,读取的结果就是t2“修改之前”的结果。
指令重排序
此原因本质上是编译器优化出bug了, 编译器觉得你写的代码太lj了,就自作主张的把你的代码调整了。
解决方案:
1.使用synchronized关键字-监视器锁
(1)修饰方法
1) 修饰普通方法 锁对象就是this
2) 修饰静态方法 所对象是类对象(Counter.class)
(2)修饰代码块 显示/手动指定锁对象
修改代码1,给类Counter1中的add方法加上synchronized锁
class Counter1{
public int count=0;
public synchronized void add(){
count++;
}
}
2.加volatile关键字
修改代码2,给flag加volatile关键字
class MyCounter{
volatile public int flag=0;
}
八、synchronized和volatile 关键字的作用
1.synchronize的作用
(1)互斥作用
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入synchronized 修饰的代码块, 相当于 加锁
退出synchronized 修饰的代码块, 相当于 解锁
(2)刷新内存
synchronized的工作过程:
1)获得互斥锁
2)从主内存拷贝变量的最新副本到工作的内存
3)执行代码
4)将更改后的共享变量的值刷新到主内存
5)释放互斥锁
所以synchronized也能保证内存可见性。 具体参见后面volatile部分。
(3)可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
2.volatile的作用
能保证内存可见性 , 那么什么是内存可见性呢?
一个线程针对一个变量进行读取从啊哦做,同时另一个线程针对这么变量进行修改,那么此时读到的值,不一定是修改之后的值~~ 这个读线程没有感知到变量的变化。比如以下代码:
import java.util.Scanner;
class MyCounter{
public int flag=0;
}
public static void main(String[] args) {
MyCounter myCounter=new MyCounter();
Thread t1=new Thread(()->{
while (myCounter.flag==0){
//循环体空着
}
System.out.println("t1循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("输入一个整数:");
myCounter.flag=scanner.nextInt();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这个代码 预期:t2把flag改为非0的值之后,t1随之就结束
实际: t1未结束
给flag变量加上volatile后,t1可以随之结束,
class MyCounter{
volatile public int flag=0;
}
给flag加上volatile关键字,意思是告诉 编译器,这个变量是“易变”的,你一定要每次都重新读取这个变量的内存内容,它指不定啥时候就变了,不敢再进行激进的优化了