第一章.多线程基本了解
1.多线程_线程和进程
进程:在内存中执行的应用程序

线程:是进程中最小的执行单元
线程作用:负责当前进程中程序的运行.一个进程中至少有一个线程,一个进程还可以有多个线程,这样的应用程序就称之为多线程程序简单理解:一个功能就需要一条线程取去执行

1.使用场景: 软件中的耗时操作 -> 拷贝大文件, 加载大量的资源
所有的聊天软件
所有的后台服务器
对于需要大量时间去完成的程序,我们可以把他放在主线程之外的程序中,这样就可以保证主线程任务的继续和耗时应用的完成。
一个线程可以干一件事,我们就可以同时做多件事了,提高了CPU的利用率
2.并发和并行
并行:在同一个时刻,有多个执行在多个CPU上(同时)执行(好比是多个人做不同的事儿)比如:多个厨师在炒多个菜

并发:在同一个时刻,有多个指令在单个CPU上(交替)执行比如:一个厨师在炒多个菜

细节:1.之前CPU是单核,但是在执行多个程序的时候好像是在同时执行,原因是CPU在多个线程之间做高速切换2.现在咱们的CPU都是多核多线程的了,比如2核4线程,那么CPU可以同时运行4个线程,此时不同切换,但是如果多了,CPU就要切换了,所以现在CPU在执行程序的时候并发和并行都存在
3.CPU调度
1.分时调度:值的是让所有的线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片
2.抢占式调度:多个线程轮流抢占CPU使用权,哪个线程先抢到了,哪个线程先执行,一般都是优先级高的先抢到CPU使用权的几率大,我们java程序就是抢占式调度
4.主线程介绍
主线程:CPU和内存之间开辟的转门为main方法服务的线程

第二章.创建线程的方式(重点)
1.第一种方式_extends Thread
1.定义一个类,继承Thread
2.重写run方法,在run方法中设置线程任务(所谓的线程任务指的是此线程要干的具体的事儿,具体执行的代码)
3.创建自定义线程类的对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法原理:Thread实现了Runnable接口,重写里面的run方法,我们继承的类需要进行重新这个方法,然后调用start方法开启这个线程。
public class Test01 {public static void main(String[] args) {//创建线程对象MyThread t1 = new MyThread();//调用start方法,开启线程,jvm自动调用run方法t1.start();for (int i = 0; i < 10; i++) {System.out.println("main线程..........执行了"+i);}}
}
public class MyThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("MyThread...执行了"+i);}}
}
2.多线程在内存中的运行原理

注意:同一个线程对象不能连续调用多次start,如果想要再次调用start,那么咱们就new一个新的线程对象
总结:需要多线程功能的代码放到继承了Thread的类重写的run方法中,然后调用这个类的实例的start方法,然后这个start方法会自己开启一个线程,开启一个栈空间,然后进行运行run中代码。
但是这里有一个缺点:
3.Thread类中的方法
void start() -> 开启线程,jvm自动调用run方法
void run() -> 设置线程任务,这个run方法是Thread重写的接口Runnable中的run方法
String getName() -> 获取线程名字
void setName(String name) -> 给线程设置名字
static Thread currentThread() -> 获取正在执行的线程对象(此方法在哪个线程中使用,获取的就是哪个线程对象)
static void sleep(long millis)->线程睡眠,超时后自动醒来继续执行,传递的是毫秒值
public class MyThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {//线程睡眠// 这里需要注意的点是:这里只能用trycatch而不能直接抛出,// 其原因在于继承的Runnable方法里面没有抛出,子类/实现类重写的方法,不能抛出比父类/接口方法更多的受检异常try {Thread.sleep(1000L);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+"...执行了"+i);}}
}
public class Test01 {public static void main(String[] args) throws InterruptedException {//创建线程对象MyThread t1 = new MyThread();//给线程设置名字t1.setName("金莲");//调用start方法,开启线程,jvm自动调用run方法t1.start();for (int i = 0; i < 10; i++) {Thread.sleep(1000L);System.out.println(Thread.currentThread().getName()+"线程..........执行了"+i);}}
}
问题:为啥在重写的run方法中有异常只能try,不能throws
原因:继承的Thread中的run方法没有抛异常,所以在子类中重写完run方法之后就不能抛,只能try,子类/实现类重写的方法,不能抛出比父类/接口方法更多的受检异常,这是基于java设计理念所规定的。
4.Thread中其他的方法
void setPriority(int newPriority) -> 设置线程优先级,优先级越高的线程,抢到CPU使用权的几率越大,但是不是每次都先抢到int getPriority() -> 获取线程优先级void setDaemon(boolean on) -> 设置为守护线程,当非守护线程执行完毕,守护线程就要结束,但是守护线程也不是立马结束,当非守护线程结束之后,系统会告诉守护线程人家结束了,你也结束吧,在告知的过程中,守护线程会执行,只不过执行到半路就结束了static void yield() -> 礼让线程,让当前线程让出CPU使用权void join() -> 插入线程或者叫做插队线程
4.1.线程优先级
public class MyThread1 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"执行了......"+i);}}
}
public class Test01 {public static void main(String[] args) {MyThread1 t1 = new MyThread1();t1.setName("金莲");MyThread1 t2 = new MyThread1();t2.setName("阿庆");/*获取两个线程的优先级MIN_PRIORITY = 1 最小优先级 1NORM_PRIORITY = 5 默认优先级 5MAX_PRIORITY = 10 最大优先级 10*///System.out.println(t1.getPriority());//System.out.println(t2.getPriority());//设置优先级t1.setPriority(1);t2.setPriority(10);t1.start();t2.start();}
}
4.2.守护线程
什么是守护线程,就是我所守护的线程没执行完我就不结束,我所守护的线程执行完了就直接结束。守护线程守护的是“整个 JVM 中是否还有非守护线程存活”,而不是某个具体的线程。
public class Test01 {public static void main(String[] args) {MyThread1 t1 = new MyThread1();t1.setName("金莲");MyThread2 t2 = new MyThread2();t2.setName("阿庆");//将t2设置成守护线程 t2.setDaemon(true);t1.start();t2.start();}
}
public class MyThread1 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"执行了......"+i);}}
}
public class MyThread2 extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+"执行了..."+i);}}
}
例2
public class DaemonDemo {public static void main(String[] args) {Thread daemon = new Thread(() -> {while (true) {System.out.println("守护线程运行中...");}});daemon.setDaemon(true); // 必须在 start() 前设置daemon.start();// 主线程(用户线程)很快结束for (int i = 0; i < 100; i++) {System.out.println("主线程结束");}}
}
应用场景
如下所示:

4.3.礼让线程
场景说明:如果两个线程一起执行,可能会执行一会儿线程A,再执行一会线程B,或者可能线程A执行完毕了,线程B在执行那么我们能不能让两个线程尽可能的平衡一点 -> 尽量让两个线程交替执行
注意:只是尽可能的平衡,不是绝对的你来我往,有可能线程A线程执行,然后礼让了,但是回头A又抢到CPU使用权了
public class Test01 {public static void main(String[] args) {MyThread1 t1 = new MyThread1();t1.setName("金莲");MyThread1 t2 = new MyThread1();t2.setName("阿庆");t1.start();t2.start();}
}
public class MyThread1 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"执行了......"+i);Thread.yield();}}
}
4.4.插入线程
public class MyThread1 extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"执行了......"+i);}}
}
public class Test01 {public static void main(String[] args) throws InterruptedException {MyThread1 t1 = new MyThread1();t1.setName("金莲");t1.start();/*表示把t1插入到当前线程之前,t1要插到main线程之前,所以当前线程就是main线程*/t1.join();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"执行了......"+i);}}
}
5.第二种方式_实现Runnable接口
1.创建类,实现Runnable接口
2.重写run方法,设置线程任务
3.利用Thread类的构造方法:Thread(Runnable target),创建Thread对象(线程对象),将自定义的类当参数传递到Thread构造中 -> 这一步是让我们自己定义的类成为一个真正的线程类对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
public class Test01 {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();/*Thread(Runnable target)*/Thread t1 = new Thread(myRunnable);//调用Thread中的start方法,开启线程t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"...执行了"+i);}}
}
public class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"...执行了"+i);}}
}
总结:这里的原理在于Thread的run是实现Runnable接口的,我们可以直接在自己的线程类中进行实现,但是实现了,还需要借助Thread进行开启这个线程,这里Thread提供了参数Runnable的构造函数,可以直接进行传入进行得出一个Thread实例,直接start即可。
6.两种实现多线程的方式区别
1.继承Thread:继承只支持单继承,有继承的局限性
2.实现Runnable:没有继承的局限性, MyThread extends Fu implements Runnable
7.第三种方式_匿名内部类创建多线程
严格意义上来说,匿名内部类方式不属于创建多线程方式其中之一,因为匿名内部类形式建立在实现Runnable接口的基础上完成的
匿名内部类回顾: 1.new 接口/抽象类(){重写方法}.重写的方法();2.接口名/类名 对象名 = new 接口/抽象类(){重写方法}对象名.重写的方法();
public class Test02 {public static void main(String[] args) {/*Thread(Runnable r)Thread(Runnable target, String name) :name指的是给线程设置名字*/new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"...执行了"+i);}}},"阿庆").start();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"...执行了"+i);}}},"金莲").start();}
}

总结:笔者认为这种写法首先是方便,因为我们可以发现,我们自己的线程类中只有一个run方法可以实现多线程,其余方法不可以进行实现,那如果我们项目仅使用一次或极少数该类的方法话,这种的优势可以很明显得出。
第三章.线程安全
1.什么时候发生:当多个线程访问同一个资源时,导致了数据有问题
1.线程安全问题-->线程不安全的代码
public class MyTicket implements Runnable{//定义100张票int ticket = 100;@Overridepublic void run() {while(true){if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}}
}
public class Test01 {public static void main(String[] args) {MyTicket myTicket = new MyTicket();Thread t1 = new Thread(myTicket, "赵四");Thread t2 = new Thread(myTicket, "刘能");Thread t3 = new Thread(myTicket, "广坤");t1.start();t2.start();t3.start();}
}
原因:CPU在多个线程之间做高速切换导致的,例如APP读取了票的数量,但是还没进行修改,就切换了官网,官网读取了数量,此时进行修改,获取了票100,切换到APP,因为之前已经读取了票的数量,就不再进行读取,直接进行操作,获取了票100。

2.解决线程安全问题的第一种方式(使用同步代码块)
1.格式:synchronized(任意对象){线程可能出现不安全的代码}
2.任意对象:就是我们的锁对象
3.执行:一个线程拿到锁之后,会进入到同步代码块中执行,在此期间,其他线程拿不到锁,就进不去同步代码块,需要在同步代码块外面等待排队,需要等着执行的线程执行完毕,出了同步代码块,相当于释放锁了,等待的线程才能抢到锁,才能进入到同步代码块中执行
4.原理:基于 Java 对象的内置锁(监视器锁,Monitor)。每个 Java 对象都与一个 Monitor 关联,线程通过获取该对象的 Monitor 来实现互斥访问。mutex是每一个对象的Monitor
具体流程如下:
锁对象:任意对象都可以作为锁,其对象头中的 Mark Word 会记录锁状态。进入同步块:线程执行 monitorenter指令(JVM 字节码),尝试获取对象的 Monitor:若 Monitor 的计数器为 0,线程成功获取锁,计数器设为 1。若线程已持有该锁(可重入),计数器递增。若锁已被其他线程占用,当前线程进入 阻塞状态(等待队列)。执行代码:持有锁的线程执行同步代码块。退出同步块:线程执行 monitorexit指令,Monitor 计数器递减:计数器归零时,锁被释放,等待队列中的线程竞争锁。未归零则仍持有锁(可重入场景)。
public class MyTicket implements Runnable{//定义100张票int ticket = 100;//任意new一个对象Object obj = new Object();@Overridepublic void run() {while(true){try {Thread.sleep(100L);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (obj){if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}}}
}
public class Test01 {public static void main(String[] args) {MyTicket myTicket = new MyTicket();Thread t1 = new Thread(myTicket, "赵四");Thread t2 = new Thread(myTicket, "刘能");Thread t3 = new Thread(myTicket, "广坤");t1.start();t2.start();t3.start();}
}
3.解决线程安全问题的第二种方式:同步方法
3.1.普通同步方法_非静态
1.格式:修饰符 synchronized 返回值类型 方法名(参数){方法体return 结果}
2.默认锁:this
3.原理:这个和上方原理一致,就是默认传入当前对象作为互斥锁,this就是当前对象
public class MyTicket implements Runnable{//定义100张票int ticket = 100;@Overridepublic void run() {while(true){try {Thread.sleep(100L);} catch (InterruptedException e) {throw new RuntimeException(e);}//method01();method02();}}/* public synchronized void method01(){if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}*/public void method02(){synchronized(this){System.out.println(this+"..........");if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}}}
public class Test01 {public static void main(String[] args) {MyTicket myTicket = new MyTicket();System.out.println(myTicket);Thread t1 = new Thread(myTicket, "赵四");Thread t2 = new Thread(myTicket, "刘能");Thread t3 = new Thread(myTicket, "广坤");t1.start();t2.start();t3.start();}
}
3.2.静态同步方法
1.格式:修饰符 static synchronized 返回值类型 方法名(参数){方法体return 结果}
2.默认锁:class对象
3.原理:synchronized方法的原理基于 Java 对象监视器(Monitor)和 Class 对象锁机制:
锁对象:静态同步方法默认使用类的Class对象(如 MyClass.class)作为锁。Class对象在JVM中是全局唯一的,所以该锁对所有实例共享。非静态同步方法使用调用该方法的对象实例(即 this)作为锁。每个对象实例在 JVM 中都有一个对应的 Monitor(监视器锁)。线程执行方法前,必须先成功获取该实例对象的 Monitor;执行完毕后自动释放。
底层实现:
当执行方法时,无论是静态还是非静态,JVM 通过方法的 ACC_SYNCHRONIZED访问标志识别同步方法。
当线程调用该方法时,JVM 会隐式执行 monitorenter(尝试获取 Class 对象的监视器锁),若锁未被占用则进入方法;若已被其他线程占用,则当前线程阻塞。方法执行结束后,JVM 自动执行 monitorexit释放锁,唤醒等待队列中的线程。
调用 methodA():
线程 T1 调用 t.methodA()。
JVM 发现方法是 ACC_SYNCHRONIZED。
JVM 获取 t这个实例对象的 Monitor。
修改 t的对象头 Mark Word,指向 Monitor。调用 methodB():
线程 T2 调用 Test.methodB()。
JVM 发现方法是 ACC_SYNCHRONIZED。
JVM 获取 Test.class这个 Class 对象的 Monitor。
修改 Test.class的对象头 Mark Word
public class MyTicket implements Runnable{//定义100张票static int ticket = 100;@Overridepublic void run() {while(true){try {Thread.sleep(100L);} catch (InterruptedException e) {throw new RuntimeException(e);}//method01();method02();}}/*public static synchronized void method01(){if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}*/public static void method02(){synchronized(MyTicket.class){if (ticket>0){System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");ticket--;}}}
}
public class Test01 {public static void main(String[] args) {MyTicket myTicket = new MyTicket();Thread t1 = new Thread(myTicket, "赵四");Thread t2 = new Thread(myTicket, "刘能");Thread t3 = new Thread(myTicket, "广坤");t1.start();t2.start();t3.start();}
}
第四章.死锁(了解)
1.死锁介绍(锁嵌套就有可能产生死锁)
指的是两个或者两个以上的线程在执行的过程中由于竞争同步锁而产生的一种阻塞现象;如果没有外力的作用,他们将无法继续执行下去,这种情况称之为死锁

根据上图所示:线程1正在持有锁1,但是线程1必须再拿到锁2,才能继续执行
而线程2正在持有锁2,但是线程2需要再拿到锁1,才能继续执行
此时两个线程处于互相等待的状态,就是死锁,在程序中的死锁将出现在同步代码块的嵌套中
2.死锁的分析

3.代码实现
public class LockA {public static LockA lockA = new LockA();
}
public class LockB {public static LockB lockB = new LockB();
}
public class DieLock implements Runnable{private boolean flag;public DieLock(boolean flag) {this.flag = flag;}@Overridepublic void run() {if (flag){synchronized (LockA.lockA){System.out.println("if...lockA");synchronized (LockB.lockB){System.out.println("if...lockB");}}}else{synchronized (LockB.lockB){System.out.println("else...lockB");synchronized (LockA.lockA){System.out.println("else...lockA");}}}}
}
public class Test01 {public static void main(String[] args) {DieLock dieLock1 = new DieLock(true);DieLock dieLock2 = new DieLock(false);new Thread(dieLock1).start();new Thread(dieLock2).start();}
}
只需要知道死锁出现的原因即可(锁嵌套),以后尽量避免锁嵌套
第五章.线程状态
1.线程状态介绍
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。
| 线程状态 | 导致状态发生条件 |
|---|---|
| NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
| Runnable(可运行,就绪态) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
| Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
| Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或者调用过时方法stop() |
2.线程状态图

