文章目录
线程相关概念
程序:是为完成特定任务,用某种语言编写的一组指令的集合,简单来说:就是我们写的代码
进程:
- 是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间,而当我们使用别的软件,就又启动了一个进程,操作系统就会为其分配新的内存空间
- 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程,又它自身的产生、存在和消亡的过程
线程
- 线程由进程创建的,是进程的一个实体
- 一个进程可以拥有多个线程
其他相关概念
- **单线程:**同一个时刻,只允许执行一个线程
- 多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以打开多个聊天窗口;一个迅雷进程,可以同时下载多个文件
- **并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单来说,**单核cpu实现的多任务就是并发
- **并行:同一个时刻,多个任务同时执行。**多核cpu可以实现并行
- 并发和并行可以同时有:假设电脑只有2个CPU,开了3个程序,那么2个CPU,只能同时执行2个任务,而另1个任务只能并发
线程的基本使用
创建线程的两种方式
在Java中线程使用有两种方式
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
线程应用案例1-继承Tread类
- 编写程序,开启一个线程,该线程每隔1秒,在控制台输出“喵喵,我是小猫咪”
- 当输出80次喵喵,我是小猫咪后,结束该线程
- 使用JConsole监控线程执行情况
package com.zanedu.threaduse;
//通过继承 Thread 类创建线程
public class Thread01 {
public static void main(String[] args) {
//创建Cat对象,可以当作线程使用
Cat cat = new Cat();
//启动线程 -> 最终会执行 Cat类的run方法
//解读源码
/*
1. public synchronized void start() {
start0();
}
2. start0() 是native方法,是本地方法,由JVM调用,底层是c/c++实现
真正实现多线程的效果,是 start0(),而不是run方法
private native void start0();
*/
cat.start();//只有start会启动线程
// cat.run();//run方法就是一个普通的方法,并没有真正的启动线程,这样就会把run方法执行完毕,才会继续向下执行,即堵塞
//说明:当main线程启动一个子线程 Thread-0,主线程不会堵塞,即主线程会继续执行
//这时 主线程和子线程是交替执行的
System.out.println("===主线程继续执行===" + Thread.currentThread().getName());//main
for (int i = 0; i < 60; i++) {
System.out.println("主线程 i=" + i);
//让主线程休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//说明
//1. 当一个类继承了 Thread类,该类就可以当作线程使用
//2. 我们会重写 run方法,写上自己的业务代码
//3. run方法是 Thread类实现了 Runnable接口的run方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
int time = 0;
@Override
public void run() { //重写run方法,写上自己的业务逻辑
while (true) {
//该线程每隔1秒,在控制台输出“喵喵,我是小猫咪”
System.out.println("喵喵,我是小猫咪" + (++time) + " 线程名=" + Thread.currentThread().getName());
//让该线程休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (time == 80) {
break;//当time到80,就退出while循环,这时线程也就退出了
}
}
}
}
- 解释说明:为什么要用start(),而不是直接调用run方法
run方法就是一个普通的方法,并没有真正的启动线程,这样就会把run方法执行完毕,才会继续向下执行,即堵塞main方法
而start会启动一个子线程 Thread-0,并且主线程不会堵塞,会继续执行下去
- 看一下start的源码
重要源码:
public synchronized void start() {
start0();
}
- 进入到start0()内部后,会去跑run方法
- start0() 是native方法,是本地方法,由JVM调用,底层是c/c++实现
- 因此真正实现多线程的效果,是 start0(),而不是run方法
- 看一下主线程和子线程的关系
- 我们可以看到,去跑代码的时候,它首先开了一个进程,然后启动了main主线程,然后又开启了子线程,当60次后主线程挂了,但是子线程还没有结束,因此还在工作,所以进程还活着,而当子线程输完80个的时候,它就挂了,即整个线程就挂了
总结:在多线程中,不是主线程挂了之后,整个线程就挂了,而是不一定,可能子线程还没有挂,因此只有所有的线程结束了,进程才结束
- JConsole一下上面代码:
- 主线程还没挂
- 主线程挂了
- start()方法调用start0()方法后,该线程并不一定立马执行,只是将线程变成了可运行状态,具体什么时候执行,取决于CPU,由CPU统一调度
线程应用案例2-实现Runnable接口
- 说明:
- Java是单继承的**,在某些情况下可能已经继承了某个父类,**这时在用继承Thread类的方法来创建线程显然不可能了
- 因此Java设计者们提供了另外一个方式来创建线程,就是通过****实现Runnable接口来创建线程
**应用案例:编写程序,该程序可以每隔1秒,在控制台输出"hi",当输出10次后,自动退出,使用实现Runnable接口的方式来实现,**这里底层使用了设计模式[代理模式]
package com.zanedu.threaduse;
//通过实现接口Runnable 来开发线程
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
// dog.start(); //这里不能调用start
//创建了Thread对象,把dog对象(实现Runnable),放入Thread
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable { //通过实现Runnable接口,开发线程
int count = 0;
@Override
public void run() { //普通方法
while (true) {
System.out.println("小狗,汪汪叫..hi" + (++count) + Thread.currentThread().getName());
//休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
- 设计模式[代理模式]
package com.zanedu.threaduse;
//通过实现接口Runnable 来开发线程
public class Thread02 {
public static void main(String[] args) {
//测试代理
Tiger tiger = new Tiger(); //实现了Runnable接口
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();
}
}
//静态代理模式
class Animal {}
class Tiger extends Animal implements Runnable{
@Override
public void run() {
System.out.println("老虎,嗷嗷叫...");
}
}
//线程代理类,模拟了一个极简的 Thread
class ThreadProxy implements Runnable{ //可以把 ThreadProxy类当作 Thread类
private Runnable target = null;//属性,类型是Runnable
@Override
public void run() {
if (target != null) {
target.run();//动态绑定 (运行类型Tiger)
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0();//这个方法是真正实现多线程的方法
}
public void start0() {
run();
}
}
线程使用应用案例-多线程执行
编写程序,创建两个线程,一个线程每隔1秒输出"hello,world",输出10次退出,一个线程每隔1秒输出"hi",输出5次退出
package com.zanedu.threaduse;
//在main线程启动2个子线程
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread = new Thread(t1);
Thread thread1 = new Thread(t2);
thread.start();//启动第一个线程
thread1.start();//启动第二个线程
}
}
class T1 implements Runnable {
int count = 0;
@Override
public void run() {
//每隔1秒输出“hello,world”,输出10次
while (true) {
System.out.println("hello, world" + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
class T2 implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
//每隔1秒输出“hi”,输出5次
System.out.println("hi" + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
break;
}
}
}
}
线程的理解
继承Thread vs 实现Runnable的区别
- 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,毕竟Thread类本身就实现了Runnable接口
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
即实现了Runnable接口的对象,可以将其传入多个线程,即可以传入这个线程,也可以传入另一个线程
- 【售票系统】,编程模拟三个售票窗口,总共售票100张,分析会有什么问题
package com.zanedu.ticket;
//使用多线程模拟三个窗口同时售票,售票100张
public class SellTicket {
public static void main(String[] args) {
// //测试Thread
// SellTicket01 sellTicket01 = new SellTicket01();
// SellTicket01 sellTicket02 = new SellTicket01();
// SellTicket01 sellTicket03 = new SellTicket01();
//
// //这里我们会出现超卖现象
// sellTicket01.start();//启动售票线程
// sellTicket02.start();//启动售票线程
// sellTicket03.start();//启动售票线程
//测试Runnable
System.out.println("===使用实现接口的方式来售票===");
SellTicket02 sellTicket02 = new SellTicket02();
new Thread(sellTicket02).start();//第1个线程 - 窗口
new Thread(sellTicket02).start();//第1个线程 - 窗口
new Thread(sellTicket02).start();//第1个线程 - 窗口
}
}
//使用Thread方式
class SellTicket01 extends Thread {
private static int ticketNum = 100;//让多个线程共享 ticketNum
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
//休眠50毫秒,模拟人休息
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}
}
//实现接口方式
class SellTicket02 implements Runnable {
private int ticketNum = 100;//让多个线程共享 ticketNum
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
//休眠50毫秒,模拟人休息
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}
}
- 出现以下这种情况是因为突然间3个窗口都进入到这个线程里,而即当只有1张票时,一边的线程还没有卖完,另一边的线程就进来了,导致出现超卖现象
- 这时我们就需要使用线程终止的方式来结束线程
线程终止
基本说明
- 当线程完成任务后,会自动退出
- 还可以通过使用变量来控制run方法退出的方式停止线程,即****通知方式
应用案例
- 需求:启动一个线程t,要求在main线程中去停止线程t
package com.zanedu.exit_;
public class ThreadExit_ {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
//希望main线程去控制 t线程的终止,就必须修改loop即可
//让 t 退出run方法,从而终止t线程 -> 通知方式
//让主线程休眠10秒,再通知t线程退出
System.out.println("===主线程休眠10秒===");
Thread.sleep(10 * 1000);
t.setLoop(false);
}
}
class T extends Thread{
private int count = 0;
//设置一个控制变量
private boolean loop = true;
@Override
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T 运行中..." + (++count));
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
线程常用方法
常用方法第一组
- setName:设置线程名称,使之与参数name相同
- getName:返回该线程的名称
- **start:使该线程开始执行,**Java虚拟机(JVM)底层调用该线程的start0()方法
- run:调用线程对象run方法
- setPriority:更改线程的优先级
- getPriority:获取线程的优先级
- sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- **interrupt:**中断线程,不是停止线程,只是抛出一个异常出来
package com.zanedu.method;
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException{
//测试相关方法
T t = new T();
t.setName("zan");
t.setPriority(Thread.MIN_PRIORITY);//设置线程的优先级
t.start();//启动子线程
//主线程输出打印 5句hi,就中断子线程的休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi" + i);
}
System.out.println(t.getName() + " 线程的优先级 = " + t.getPriority());
t.interrupt();//当执行到这里,就会中断 t线程的休眠
}
}
class T extends Thread { //自定义的线程类
@Override
public void run() {
while (true) {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~~~");
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中~~~~");
Thread.sleep(20000);//20秒
} catch (InterruptedException e) {
//当该线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
//InterruptedException 是捕获到到一个中断异常
System.out.println(Thread.currentThread().getName() + "被 interrupt了");
}
}
}
}
- 使用interrupt,中断线程,即在这块是使休眠被中断,即线程继续
注意事项和细节
- **start底层会创建新的线程,调用run,**而run方法就是一个简单的方法调用,不会启动新线程
- 线程优先级的范围 - 3类
- interrupt:中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程
- sleep:线程的静态方法,使当前线程休眠
常用方法第二组
- yield:线程的礼让,礼让cpu,让其他线程执行,但礼让的时间不确定,所以****不一定能礼让成功
- **join:线程的插队。插队的线程一旦插队成功,**则肯定先执行完插入的线程所有的任务,才会继续执行别的任务
package com.zanedu.method;
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException{
T2 t2 = new T2();
t2.start();
for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("主线程 吃了 " + i + " 个包子");
if (i == 5) {
System.out.println("主线程让子线程先吃");
//join,线程插队
//t2.join();//这里相当于让子线程先执行完毕
Thread.yield();//礼让,不一定成功
System.out.println("子线程吃完了,主线程再接着吃");
}
}
}
}
class T2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程 吃了 " + i + " 个包子");
}
}
}
- yield没有礼让成功
- join插队,先完成子线程,再跑主线程
用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
package com.zanedu.method;
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
//如果我们希望当main主线程结束后,子线程可以自动结束
//只需要将子线程设置为守护线程即可
//注意:要先设置,再启动子线程
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for (int i = 1; i <= 10; i++) { //main线程
System.out.println("宝强在辛苦的工作...");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
for(; ; ) { //无限循环
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("马蓉和宋喆快乐聊天,哈哈哈~~~");
}
}
}
线程的生命周期
JDK中用Thread.State枚举表示了线程的几种状态
- 从官网文档看线程有6种状态,而更细化的话,可以将Runnable状态划分为Ready状态和Running状态,即7种状态
线程状态转换图(重点-涉及操作系统)
线程的同步-synchronized
线程同步机制
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性
- **也可以这样理解:线程同步,**即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
同步具体方法-synchronized
- 同步代码块
synchronized (对象) { //得到对象的锁,才能操作同步代码
//需要被同步代码;
}
- synchronized还可以放在方法声明中,表示整个方法为同步方法
public synchronized void m(String name) {
//需要被同步的代码
}
- 理解:好比某小伙伴上厕所先把门关上(上锁),完事后再出来(解锁),那么其他小伙伴就可以使用厕所了
分析同步原理
- 比如t1抢到了锁,然后将锁关闭,只有当t1出来的时候,t2、t3才有可能抢到锁进去,但是同时t1也可以再次抢锁,依次类推
互斥锁
基本介绍
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为"互斥锁"的标记,整个标记用来保证在任一时刻,只能有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系。当某个对象要用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低【由于会堵塞,只有抢到锁才能出去】
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(但要求是同一对象)
- 同步方法(静态的)的锁为当前类本身
使用互斥锁解决售票问题
package com.zanedu.syn;
//使用多线程模拟三个窗口同时售票,售票100张
public class SellTicket {
public static void main(String[] args) {
//测试一把
System.out.println("===使用synchronized实现线程同步来售票===");
SellTicket03 sellTicket03 = new SellTicket03();
new Thread(sellTicket03).start();//第1个线程 - 窗口
new Thread(sellTicket03).start();//第1个线程 - 窗口
new Thread(sellTicket03).start();//第1个线程 - 窗口
}
}
//实现接口方式,使用synchronized实现线程同步
class SellTicket03 implements Runnable {
private int ticketNum = 100;//让多个线程共享 ticketNum
private boolean loop = true;//控制run方法的变量
Object object = new Object();
//6. 同步方法(静态的)的锁为当前类本身
//解读
//1. public synchronized static void m1() {} 锁是加在 SellTicket03.class
//2. 如果在静态方法中,如果要实现一个同步代码块
/*
synchronized (SellTicket03.class) {
}
*/
public synchronized static void m1() {
}
public static void m2() {
synchronized (SellTicket03.class) {
}
}
//说明
//1. public synchronized void sell() {} 就是一个同步方法
//2. 这时锁就在this对象
//3. 也可以在代码块上写 synchronized ,同步代码块,互斥锁还是在this对象
public /*synchronized*/ void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法
//同步方法(非静态的)的锁可以是this,也可以是其他对象(但要求是同一个对象)
synchronized (/*this*/ object) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
//休眠50毫秒,模拟人休息
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
//使用Thread方式
//new SellTicket01.start()
//new SellTicket01.start()
class SellTicket01 extends Thread {
private static int ticketNum = 100;//让多个线程共享 ticketNum
public void m1() {
synchronized (this) {
}
System.out.println("hello");
}
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
//休眠50毫秒,模拟人休息
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}
}
注意事项和细节
- 同步方法如果没有使用static修饰:默认锁的对象为this
- 同步方法如果使用static修饰:默认锁的对象为 当前类.class
- 实现的落地步骤
-
需要先分析上锁的代码
-
选择同步代码块或同步方法
-
要求多个线程的锁对象为同一个
-
不是同一个对象,会抛出异常
线程的死锁
- 基本介绍
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程中一定要避免死锁的发生
- 案例
妈妈:你先完成作业,我让你玩手机
小明:你先让我玩手机,我再完成作业
package com.zanedu.syn;
//模拟线死锁
public class DeadLock_ {
public static void main(String[] args) {
//模拟死锁现象
DeadLockDemo a = new DeadLockDemo(true);
a.setName("A线程");
DeadLockDemo b = new DeadLockDemo(false);
b.setName("B线程");
a.start();
b.start();
}
}
//线程
class DeadLockDemo extends Thread {
static Object o1 = new Object();//保证多线程,共享一个对象,这里使用static
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//分析
//1. 如果flag 为 T,该线程A就会先得到/持有 o1 对象锁,然后尝试去获取 o2 对象锁
//2. 如果线程A得不到 o2 对象锁,就会Blocked
//3. 如果flag 为 F,线程B就会先得到/持有 o2 对象锁,然后回去尝试获取 o1 对象锁
//4. 如果线程B拿不到 o1 对象锁,就会Blocked
if (flag) {
synchronized (o1) { //对象互斥锁,下面就是同步代码
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
释放锁
下面操作会释放锁
- 当前线程的同步方法、同步代码块****执行结束
案例:上厕所,完事出来
- 当前线程在同步代码块、同步方法种****遇到break、return
案例:没有正常的完事,经理叫他修改bug,不得已出来
- 当前线程在同步代码块、同步方法种****出现了未处理的Error和Exception,导致异常结束
案例:没有正常的完事,发现忘带纸,不得已出来
- 当前线程在同步代码块、同步方法种****执行了线程对象的wait()方法,当前线程暂停,并释放锁
案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去
下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位上眯了一会
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
提示:尽量避免使用suspend()和resume()来控制线程,因为方法不再推荐使用,被弃用了