目录
前言:
早期的(命令行)操作系统不能一次性执行多个任务,引入“进程”的概念后,实现了多任务。
我们可以把进程看作一个应用程序,运行一个应用程序时,操作系统会开启一个独立的进程,给这个进程划分独立的内存地址空间。除了我们开启的应用程序以外,操作系统自己的后台程序也都是一个个进程(包括病毒程序),在进程内部再划分多任务就是线程,它可以让一个程序同时做多件事
多进程在执行的时候,并不是绝对的同时执行,只是因为CPU在多个进程之间来回的快速切换,速度很快,感觉不到停顿罢了。
Java中的多线程
JVM是一个进程
启动一个Java应用程序,就是启动了一个JVM。应用程序是在JVM内部运行的。JVM是一个进程,我们书写的代码又是一个泡在JVM中的一个线程。
在JVM当中除了我们书写的main线程以外,同时还跑了多个其他线程。比如:GC线程(它是JVM中的一个线程,作用是回收我们在程序中的产生的垃圾对象)
Java的多线程开发,可以让我们在main线程中再次开启多个子线程,从而执行不同的动作。所以程序是由main方法开始而开始,但是main方法结束不一定程序结束,因为可能还有子线程没有结束。
所有的线程一旦启动,都是平行关系,CPU会平等的在它们之间来回切换,我们可以调整比例但无法绝对控制。
在多线程的情况下,程序执行的效果具备一定的随机性,同一段代码每次执行的效果可能不一样,因为CPU和硬件的自身算法所带来的,甚至不同的机器也会不一样。
线程的生命周期
1、新建
new Thread() 对象
向JVM申请要建立一个新的线程,JVM就会做一些资源的申请和分配动作。
2、就绪
Thread.start()
建好后,启动线程,也要先执行一段底层代码,让这个线程运行起来,然后才能执行我们在代码书写的执行任务。因此,从线程启动开始,到它真正执行我们书写的线程内部代码之前,这个阶段叫做 就绪。就绪状态是在start()开始,到他调用run()之前。
3、运行
运行状态就是run()方法的执行周期,从run()开始到run()结束。可以把run()方法看成是这个子线程的main方法。
4、中断
在run()的执行过程中,这个代码可能没有得到CPU的执行,但是run又没有结束,那么这个时候这个线程没有死,在等待,等待CPU再次继续执行它。
5、死亡
run()方法结束后,Thread对象会自动调用它销毁线程,回收资源的底层代码实现。(这个过程我们无法控制)
多线程实现的API
一、继承 Thread
步骤
1.继承 Thread
2.重写run方法
3.调用start方法
注意
- 子线程被开启以后,主线程没有停,依然会和子线程抢夺CPU,继续往下执行
- 一个线程可以产生多个线程对象,每个线程对象启动就是一个子线程,执行的是同样的代码
- setName()给线程取名
- setPriority() 给线程设置优先级(共10级,默认都是5级),优先级高的,受到CPU的选择几率高。
二、实现 Runnable接口
Java的继承是单继承,在某些场景我们需要让线程拥有某个业务父类,就没办法同时继承Thread类了。且Runnable很容易实现资源共享
步骤
1、让类实现接口
2、产生该 实现类的对象 然后作为参数传递到真正的Thread身上
3、调用Thread对象的start()方法
分情况使用
一、外部单独调用start方法
public class Main {
public static void main(String[] args) {
Thread thread1=new Thread(new A());
Thread thread2=new Thread(new B());
thread1.start();
thread2.start();
}
}
public class A implements Runnable {
@Override
public void run() {
线程需要做的事
}
}
public class B implements Runnable{
@Override
public void run() {
线程需要做的事
}
}
二、直接在线程类的构造方法中调用start方法
- 线程类对象一旦产生立马启动运行
public class Main {
public static void main(String[] args) {
new A();
new B();
}
}
public class A implements Runnable {
public A() {
Thread thread=new Thread(this);
thread.start();
}
@Override
public void run() {
线程需要做的事
}
}
public class B implements Runnable{
public B() {
Thread thread=new Thread(this);
thread.start();
}
@Override
public void run() {
线程需要做的事
}
}
三、匿名内部类
- 只用一次
public class Main {
new Thread(new A()){
@Override
public void run() {
线程需要做的事
}
}.start();
}
1、本质上,线程必须使用Thread这个类,必须有它的对象和调用它的start()方法;
2、Thread类在没有传入Runnable接口的实现类对象时,是执行自己的run方法;传入了Runnable接口的实现类对象,就执行这个实现类的run方法;
三、实现 Callable接口
它可以让主线程获取到子线程的状态
前两种的run方法返回类型都是void,它们调用的start也是void。这样的主线程是得不到子线程的结果的,如果子线程报了异常,主线程也得不到。
步骤
1、实现Callable接口,重写call方法。call方法中书写线程要做的事,它可以返回一个唯一结果,且这个结果是返回给主线程的;
【Callable<> 泛型就是定义要返回的数据类型】
2、把这个实现类的对象 传递给FutureTask类的构造方法中
【FutureTask的泛型应该与Callable的返回类型保持一致】
3、把FutureTask的对象传递给Thread类的构造方法。然后调用Thread对象的start方法
4、调用FutureTask对象的get方法获取到子线程call方法执行结束以后的返回值
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> ft=new FutureTask(new A());
Thread td=new Thread(ft);
td.start();
String result=ft.get();
System.out.println(result);
}
}
public class A implements Callable<String> {
@Override
public String call() throws Exception {
书写线程需要做的事
String str="需要返回的结果";
return str;
}
}
注意
1.当主线程已经调用FutureTask对象的get方法获取返回结果了,而子线程还没有执行完,那么这个时候主线程会进入阻塞状态,等待子线程全部执行结束,拿到返回值之后再往下执行
2.如果没有使用Callable,而是使用另外两个,也想得到主线程在子线程执行结束后再执行,可以调用子线程对象的join方法,达到此效果。
多线程引发的问题
多线程,让应用程序的丰富度也增加了,但是它会有线程安全性问题
注意
1、单线程一定没有线程安全性问题
2、多线程不一定有线程安全性问题(线程安全性问题是一种特殊场景)当多个线程操作统一资源时,才有可能发生。
线程安全性问题发生的主要原因
一、可见性问题
程序是在内存中执行的,声明的数据是在内存中,存放的数据在硬盘上,执行的数据是在CPU当中,越靠近CPU的硬件,执行速度越快
在Java的内存模型上,其实CPU也有存放数据的缓存
二、原子性问题
必须让一个线程中的多段代码被单独执行,不能被其他线程的代码插入,让这段代码成为独立的不可分隔的原子步骤。
三、有序性问题
我们书写代码的顺序,不一定是计算机执行指令的顺序,硬件方的指令可能对此进行优化
问题的处理
1、让一个线程对这个资源的改变,能够及时通知另外一个资源的改变,让它不要一直去读它的缓存数据
2、让整段代码或一句代码被拆分成的多个指令不被打断
线程安全性问题的解决
关键字 volatile
- 不稳定的
解决 可见性问题,它可以修饰变量(在多线程中要操作的那个同一资源变量)
public class ThreadA extends Thread{
private volatile boolean isRun = true;//修饰变量,要操作的 同一资源变量
public void setRun(boolean run) {
isRun = run;
}
@Override
public void run() {
System.out.println("线程1开始执行.........");
while(isRun){//isRun为false时,跳出循环,才执行下面输出语句
}
System.out.println("线程1结束执行");
}
}
public class ThreadB extends Thread{
private ThreadA threadA;
public ThreadB(ThreadA threadA){
this.threadA = threadA;
}
@Override
public void run() {
System.out.println("线程2启动执行........");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.setRun(false);//改变资源变量
System.out.println("线程2结束执行......");
}
}
public class Test {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2(t1);
t1.start();
t2.start();
}
}
关键字 synchronized
- 同步
一段代码在执行的过程中,只能由一个线程完成。其他线程,进入等待状态。当这个线程把这段代码执行完成了,其他线程才可以进入。这段代码只能是一个线程一个线程的来,不存在被插入的可能性。保证了它的原子性和有序性
同步方法
把synchronized写在资源的某个方法的声明处(相当于修饰符的位置)。那么这个方法每次只能被一个线程调用,其他线程等待。执行完一个线程后,其他线程才能调用。
同步块
把synchronized以代码块的形式,写在线程的调用处
synchronized(资源对象){
资源对象.方法1();
资源对象.方法2();
}
小练习
线程同步--同步方法
public class BigPang2 implements Runnable{
private String name;
private long loadTime;
private Dumplings2 dps;
private int num;
public BigPang2() {
}
public BigPang2(String name, long loadTime, Dumplings2 dps) {
Thread thread=new Thread(this);
thread.start();
this.name=name;
this.loadTime=loadTime;
this.dps=dps;
}
@Override
public void run() {
while (this.dps.eat()){
num++;
System.out.println(name+"吃了"+num+"个");
try {
Thread.sleep(loadTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//资源类
public class Dumplings2 {
private int dumplingNum=25;
public synchronized boolean eat(){
//保证两句代码不可分隔
if(this.dumplingNum>0){
this.dumplingNum--;
return true;
}else {
return false;
}
}
}
public class Main2 {
public static void main(String[] args) {
Dumplings2 dumplings2 = new Dumplings2();
new BigPang2("大胖",200,dumplings2);
new SmallShou2("小瘦",300,dumplings2);
}
}
线程同步--同步块
public class SmallShou implements Runnable{
private String name;
private long loadTime;
private Dumplings dps;
public SmallShou() {
}
public SmallShou(String name, long loadTime, Dumplings dps) {
Thread thread=new Thread(this);
thread.start();
this.name=name;
this.loadTime=loadTime;
this.dps=dps;
}
@Override
public void run() {
int num=0;
while (true){
//保证 判断饺子数量的if语句 和 数量-1的代码 不会被插入,不可分隔
synchronized (this.dps) {
if(this.dps.getDumplingNum()>0) {
this.dps.setDumplingNum(this.dps.getDumplingNum() - 1);
}else {
break;
}
}
num++;
System.out.println(this.name + "吃了第" + num + "个饺子 "
+ "还剩" + this.dps.getDumplingNum() + "个");
try {
Thread.sleep(loadTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("小瘦吃了"+num+"个");
}
}
资源类
public class Dumplings {
private int dumplingNum=25;
public int getDumplingNum() {
return dumplingNum;
}
public void setDumplingNum(int dumplingNum) {
this.dumplingNum = dumplingNum;
}
}
public class Main {
public static void main(String[] args) {
Dumplings dumplings = new Dumplings();
new BigPang("大胖",200,dumplings);
new SmallShou("小瘦",300,dumplings);
}
}
问题
进程与线程的区别?
1、线程是在进程内部再次进行多任务的划分;
2、进程是独立内存地址空间的; 同一个进程内部的线程是共享内存地址空间的;
3、CPU在进程之间进行切换的成本高于在线程之间进行切换。
线程中断状态的原因?
1.CPU的切换
2.由于输入或输出导致的阻塞状态
3.让一个线程主动进入休眠状态,给它设置一个休眠的时长 Thread里的sleep()方法;
4.多个线程访问同一资源时,可以让A线程主动退出,进入等待;B线程进入执行资源,B线程执行结束以后,再唤醒A线程。
Object里的 wait()方法【等待】 和 notify()方法【唤醒第一个排队等待的】notifyAll()【唤醒所有】
它们只能写在同步方法或同步块中
为什么线程安全的类效率更低?
它把并行的多个线程,变成了排队,依次执行的串行状态。多个线程在等待,每次只会唤醒其中一个。按等待的先后顺序唤醒的叫“公平锁”,synchronized是“非公平的锁”。