15. 多线程基础
15.1 线程相关概念
-
程序(program)
是为了完成特定任务,用某种语言编写的一组指令的集合,简单的说就是我们的代码
-
进程
-
进程是
指运行中的程序
,比如我们使用的QQ
,双击QQ.exe文件
,便启动了一个进程
,操作系统会为该进程分配内存空间,又如我们写了一段代码
,然后编译运行
,也是启动了一个进程
-
进程是程序的一次执行,或是正在运行的一个程序,是动态过程:有它自身的产生,存在和消亡的过程
-
-
线程
-
线程由进程创建
,是进程的一个实体,如下载和上传,可以同时下载多个文件,这就产生了多个线程,又如QQ聊天时的多个窗口,你可以跟A同学聊天,同时又跟B同学聊天,也是线程 -
一个进程可以有多个线程,便有单线程和多线程的概念,如下图
-
单线程:同一时刻,只能执行一个线程
-
多线程:同一时刻,可以执行多个线程,比如一个QQ进程可以打开多个聊天窗口
-
-
-
并发
-
同一时刻,多个任务交替执行,造成一种 “貌似同时” 的错觉,简单的说就是单核CPU实现的多任务就是并发
-
如下图所示,单核CPU,它要执行两个任务,因此它只能一会执行QQ,一会执行迅雷这样交替进行,就是并发,也可以比作人的大脑,边开车边打电话就是并发
-
-
并行
-
同一时刻,多个任务同时执行
,多核CPU可以实现并行,或者并发和并行 -
例如下图,对于双核CPU,如果再开一个进程,比如说微信,对于上面的CPU可能就是并发的(交替)执行QQ和微信,但是对于CPU执行QQ和迅雷来说就是并行的
-
15.2 线程基本使用
15.2.1 创建线程的方式
在Java中创建线程的方式有两种
-
继承
Thread
类,重写run
方法(Thread
意思即线程)class Dog extends Thread { @Override public void run() { //super.run(); 一般不调用父类的 } }
-
实现
Runnable
接口,重写run
方法,其实Thread
类也是实现了Runnable
接口class Dog implements Runnable { @Override public void run() { } }
15.2.2 继承Thread类
步骤(以下面的代码为基础理解):
-
写一个类继承
Thread
类,并重写run
方法 -
创建一个该类的对象,此时便可以当做线程来使用
-
cat.start
方法用来启动线程,内部代码会调用start0()
这个方法,然后再在start0()
这个方法中以特殊的方式调用run
方法,若直接调用 run 只是普通的方法调用而已
-
start0()
是本地方法,是JVM调用, 底层是c/c++实现private native void start0();
-
真正实现多线程的效果, 是
start0()
, 而不是run
-
说明:
-
当一个类继承了 Thread 类, 该类就可以当做线程使用
-
我们会重写 run方法,写上自己的业务代码
-
run Thread 类 实现了 Runnable 接口的run方法
-
当主线程结束了,子线程不会结束,会继续执行
,且子线程中可以再创建线程
阅读代码,注释及执行结果理解线程的使用:
public class Main {
public static void main(String[] args) throws InterruptedException {
// 创建Cat对象,可以当做线程使用
Cat cat = new Cat();
cat.start(); // 启动线程 -> 最终会执行cat的run方法
// 理解线程:执行cat.start之后,开启另一个线程,原本只有一个线程
// 即main函数这里这个主线程,开启线程后,下面的代码将会和run里的代码
// 会交替执行,即主线程和子线程交替执行
// cat.run();//run方法就是一个普通的方法,如果只是这样普通调用run方法
// 没有真正的启动一个线程,就会把run方法执行完毕才向下执行,因此不直接调用run方法
// 说明: 当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
// 这时 主线程和子线程是交替执行..
for(int i = 1; i<= 5; i++) {
// Thread.currentThread().getName() 获取当前线程的名字
Thread.sleep(100); //休眠100ms
System.out.println(Thread.currentThread().getName() + "主线程" + i);
}
}
}
class Cat extends Thread {
int count = 0;
@Override
public void run() {
while(true) {
if(count == 5) break;
System.out.println(Thread.currentThread().getName() +
"子线程:小猫喵喵叫..." + (++count));
try {
Thread.sleep(100); //线程休眠100ms,1000ms=1s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上代码执行结果:
15.2.3 实现Runnable接口
-
基本介绍
- java 是单继承机制,在某些的情况下一个类可能已经继承了另一个类,这时便不能在继承
Thread
类了,用继承Thread
的方法创建线程就不可能了 - javad设计者便提供了另一种方式创建线程,就是通过实现
Runnable
接口来实现线程
- java 是单继承机制,在某些的情况下一个类可能已经继承了另一个类,这时便不能在继承
-
举例讲解
-
写法一
:先创建一个dog
,在new
一个Thread
,然后在调用Thread
的start
函数//这里用到了静态代理模式 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() { //普通方法 //写自己的业务逻辑代码 } }
-
方法二:直接在类中维护一个私有的
Thread
对象,然后写一个start
(注意这不是重写),在方法中初始化该Thread
对象,然后调用它的start
方法public class test { public static void main(String[] args) { Dog dog = new Dog(); dog.start(); for(int i=1;i<1000;i++) { System.out.println("000"); } // Thread类的start方法只能调用一次,如果下面的t.start放外面了的话 // 那这里第二次调用dog.start就会报错 // dog.start(); } } class Dog implements Runnable { private Thread t; //维护一个私有的Thread对象 @Override public void run() { for(int i=0;i<10;i++) { System.out.println("1"); } } public void start() { if(t==null) { t = new Thread(this); t.start(); //注意这个是放里面,而不是外面,保证start方法只被调用一次 } } }
静态代理设计模式的简单理解:
public class Thread02 { public static void main(String[] args) { Tiger tiger = new Tiger();//实现了 Runnable ThreadProxy threadProxy = new ThreadProxy(tiger); threadProxy.start(); } } //线程代理类 , 模拟了一个极简的Thread类 class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxy 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(); } }
-
15.2.4 JConsole监控线程
以15.2.2继承Thread类
中的代码为例
:
首先将循环结束的条件改大点或休眠时间长点,避免还没打开JConsole线程已经执行完了
步骤
-
开始运行程序后,点击
Terminal
(终端) -
输入
JConsole(不分大小写)
回车,便会进入Java监视和管理控制台窗口
,然后再窗口中选择本地进程,便会在里面看见当前进程即com.qingtian.demo1.Main(包名+类名)
,选择后点击连接即可 -
连接中会出现这个情况,直接不安全连接即可
-
之后在左上角选择线程,然后在左下角可以找到main和Thread-0,即两个线程的名字
-
当main线程执行结束时,此时便只剩下Thread-0在执行
-
最后Thread-0也执行完,之后便失去连接,并且连不上了
15.2.5 Thread 与 Runnable
-
多线程的理解
-
继承 Thread vs 实现 Runnable 的区别
-
两者面临的问题:
线程同步问题
public class SellTicket { public static void main(String[] args) { //测试 // SellTicket01 sellTicket01 = new SellTicket01(); // SellTicket01 sellTicket02 = new SellTicket01(); // SellTicket01 sellTicket03 = new SellTicket01(); // // //这里我们会出现超卖.. // sellTicket01.start();//启动售票线程 // sellTicket02.start();//启动售票线程 // sellTicket03.start();//启动售票线程 // //这里也会出现超卖.. System.out.println("===使用实现接口方式来售票====="); SellTicket02 sellTicket02 = new SellTicket02(); new Thread(sellTicket02).start();//第1个线程-窗口 new Thread(sellTicket02).start();//第2个线程-窗口 new Thread(sellTicket02).start();//第3个线程-窗口 } } //使用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));//1 - 0 - -1 - -2 } } }
15.2.6 线程的终止
-
基本说明
- 当线程完成任务后,会自动退出
- 还可以通过使用变量来控制
run
方法退出的方式来停止线程,即通知方式
-
举例说明
public class ThreadExit_ { public static void main(String[] args) throws InterruptedException { T t1 = new T(); t1.start(); //如果希望main线程去控制t1 线程的终止, 必须可以修改 loop //让t1 退出run方法,从而终止 t1线程 -> 通知方式 //让主线程休眠 10 秒,再通知 t1线程退出 System.out.println("main线程休眠10s..."); Thread.sleep(10 * 1000); t1.setLoop(false); } } class T extends Thread { private int count = 0; //设置一个控制变量 private boolean loop = true; @Override public void run() { while (loop) { try { Thread.sleep(50);// 让当前线程休眠50ms } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("T 运行中...." + (++count)); } } public void setLoop(boolean loop) { this.loop = loop; } }
15.3 线程常用方法
15.3.1 常用方法第一组
-
基本介绍
方法 作用 setName
设置线程名称 getName
返回该线程名称 start
使该线程开始执行,java虚拟机底层调用该线程的start0方法 run
调用线程对象的run方法 setPriority
更改线程的优先级,优先级有三个常量,可以自己看源码 getPriority
获取线程的优先级 sleep
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) interrupt
中断休眠,提前结束sleep休眠,注意不是终止线程 -
注意细节
-
start
底层会创建新的线程,调用run
,run
就是一个简单的方法调用,不会启动新的线程 -
线程优先级的范围
-
interrupt
结束线程的休眠,相对于唤醒 -
sleep
:线程的静态方法,使当前线程休眠
-
-
举例说明
public class ThreadMethod01 { public static void main(String[] args) throws InterruptedException { //测试相关的方法 T t = new T(); t.setName("老韩"); t.setPriority(Thread.MIN_PRIORITY);//1 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());//1 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() + "吃包子~~~~" + i); } try { System.out.println(Thread.currentThread().getName() + "休眠中~~~"); // 这里会休眠20秒,但是被main里面的interrupt,便抛出一个异常,之后便执行catch Thread.sleep(20000);//20秒 } catch (InterruptedException e) { //当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码 //InterruptedException 是捕获到一个中断异常. System.out.println(Thread.currentThread().getName() + "被 interrupt了"); } } } }
15.3.2 常用方法第二组
-
基本介绍
-
yield
:线程的礼让,让出cpu,让其线程执行,但礼让的时间的不确定,所以也不一定礼让成功 -
join
:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有任务,在执行被插队的线程,注意join 方法是在其他线程中调用,如线程t1调用线程t2的join的方法,意思就是先把t1线程占用的CPU让给t2先执行完,再执行t1
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的线程在执行这里的 //t2.join();// 这里相当于让t2 线程先执行完毕 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 + " 包子"); } } }
-
15.3.3 用户线程和守护线程
-
基本介绍
-
用户线程:也叫工作线程,当线程的
任务执行完
或通知方式介绍
-
守护线程:一般是为工作线程服务,当所有的用户线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
-
-
举例讲解
public class ThreadMethod03 { public static void main(String[] args) throws InterruptedException { MyDaemonThread myDaemonThread = new MyDaemonThread(); //如果我们希望当main线程结束后,子线程自动结束 //,只需将子线程设为守护线程即可 myDaemonThread.setDaemon(true); //将这个线程设置成守护线程 myDaemonThread.start(); //main线程,设置守护线程后,若下面的执行玩了,那子线程即无限for循环也会结束 for( int i = 1; i <= 10; i++) { System.out.println("宝强在辛苦的工作..."); Thread.sleep(1000); } } } class MyDaemonThread extends Thread { public void run() { for (; ; ) {//无限循环 try { Thread.sleep(1000);//休眠1000毫秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("马蓉和宋喆快乐聊天,哈哈哈~~~"); } } }
15.4 线程的生命周期
-
基本介绍
-
线程状态转换图
解释说明:
- 新new的一个对象,还没有调用
start
方法前处于New
状态 - 调用的
start
的线程处于Runnable
状态,其中Runnable
状态又可以细分为Ready
(准备状态)和Running
状态(运行状态) - 在
Runnable
状态的线程调用Thread.sleep()
或者其他方法则该线程将会进入TimeWaiting
状态 - 在
Runnable
状态的线程调用自身的wait()
方法或者在自身线程里调用其他线程的join()
方法,就会进入Waiting
状态 - 等待进入同步代码块,即等待对象锁时,就会处于
Blocked
状态(堵塞状态) - 线程结束后,便进入了
Terminated
状态
- 新new的一个对象,还没有调用
-
举例查看线程状态图
public class ThreadState_ { public static void main(String[] args) throws InterruptedException { T t = new T(); System.out.println(t.getName() + " 状态 " + t.getState()); t.start(); while (Thread.State.TERMINATED != t.getState()) { System.out.println(t.getName() + " 状态 " + t.getState()); Thread.sleep(500); } System.out.println(t.getName() + " 状态 " + t.getState()); } } class T extends Thread { @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("hi " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
15.5 线程的同步
-
基本介绍
解决15.2.5中多卖出了票的问题
-
在多线程编程中,
一些敏感数据不允许被多个线程同时访问
,此时就需要使用同步访问技术保证数据在任何一时刻,最多有一个线程访问,以保证数据的完整性 -
理解:线程同步,即
当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
-
-
同步具体方法-Synchronized
-
同步代码块
synchronized(对象) { //得到对象锁,才能操作同步代码 需要被同步的代码 }
-
synchronized
还可以放在方法声明中,表示整个方法为同步方法public synchronized void m(String name) { 需要被同步的代码 }
-
-
分析同步原理
15.6 互斥锁
-
基本介绍
- Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应与一个可称为 “互斥锁” 的标记,这个标记用来保证在任一时刻,只能一个线程访问该对象
- 关键字
synchronized
来与对象的互斥锁联系,当某个对象用synchronized
修饰时,表明该对象在任意时刻只能由一个线程访问 - 同步的局限性:导致程序的执行效率降低
- 同步方法(非静态的)的锁可以是
this
,也可以是其他对象(要求是同一个对象
) - 同步方法(静态的)的锁为当前类本身
-
举例讲解
使用互斥锁来解决售票问题
public class SellTicket { public static void main(String[] args) { //测试一把,都是用同一对象创建的三个线程,因此下面的对象锁object也是同一个 SellTicket03 sellTicket03 = new SellTicket03(); new Thread(sellTicket03).start();//第1个线程-窗口 new Thread(sellTicket03).start();//第2个线程-窗口 new Thread(sellTicket03).start();//第3个线程-窗口 } } //实现接口方式, 使用synchronized实现线程同步 class SellTicket03 implements Runnable { private int ticketNum = 100;//让多个线程共享 ticketNum private boolean loop = true;//控制run方法变量 Object object = new Object(); //同步方法(静态的)的锁为当前类本身 //老韩解读 //1. public synchronized static void m1() {} 锁是加在 SellTicket03.class //2. 如果在静态方法中,实现一个同步代码块. /* synchronized (SellTicket03.class) { System.out.println("m2"); } */ public synchronized static void m1() { } public static void m2() { synchronized (SellTicket03.class) { System.out.println("m2"); } } //老韩说明 //1. public synchronized void sell() {} 就是一个同步方法 //2. 这时锁在 this对象 //3. 也可以在代码块上写 synchronize ,同步代码块, 互斥锁还是在this对象 public /*synchronized*/ void sell() { //同步方法, 在同一时刻, 只能有一个线程来执行sell方法 // 因为是同一个对象,所有成员变量object也是同一个对象 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));//1 - 0 - -1 - -2 } } @Override public void run() { while (loop) { sell();//sell方法是一共同步方法 } } }
-
注意事项
- 同步方法如果没有使用
static
修饰:默认锁对象为this
- 如果方法使用
static
修饰,默认锁对象:当前类.class
- 实现的步骤:
- 需要先分析上锁的代码
- 选择同步代码块或同步方法
要求多个线程锁对象为同一个即可
- 同步方法如果没有使用
15.7 线程的死锁
-
基本介绍
多个线程都占用了对方的锁资源,但是不肯相让,导致了死锁,在编程是一定要避免死锁发生的
-
举例讲解
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) { // 这里获得li对象的监视权 System.out.println(Thread.currentThread().getName() + " 进入2"); } } } else { synchronized (o2) { System.out.println(Thread.currentThread().getName() + " 进入3"); synchronized (o1) { // 这里获得li对象的监视权 System.out.println(Thread.currentThread().getName() + " 进入4"); } } } } }
15.8 释放锁
-
下面操作会释放锁
-
当前线程的同步方法,同步代码块执行结束
类比:上厕所,完事出来
-
当前线程在同步代码块,同步方法中遇到
break
,return
类比:没有正常完事,经理叫他修改bug,不得已出来
-
当前线程在同步代码块,同步方法中出现了未处理的
Error
或Exception
,导致异常结束类比:没有正常完事,发现忘带纸,不得已出来
-
当前线程在同步代码块,同步方法中执行了线程对象的
wait()
方法,当前线程暂停,并释放锁类比:没有正常完事,觉得需要酝酿,所以出来等会再进去
-
-
下面操作不会释放锁
-
线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
,Thread.yield()
方法暂停当前线程的执行,不会释放锁类比:上厕所,太困了,在坑位上眯了会
-
线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁注意:应尽量避免使用
suspend()
和resume()
来控制线程,方法不在推荐使用
-