Java学习笔记-多线程基础
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 {@Overridepublic void run() {//super.run(); 一般不调用父类的} }
-
实现
Runnable
接口,重写run
方法,其实Thread
类也是实现了Runnable
接口class Dog implements Runnable {@Overridepublic 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); //休眠100msSystem.out.println(Thread.currentThread().getName() + "主线程" + i);}}
}class Cat extends Thread {int count = 0;@Overridepublic 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),放入ThreadThread thread = new Thread(dog);thread.start();} }class Dog implements Runnable { //通过实现Runnable接口,开发线程int count = 0;@Overridepublic 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对象@Overridepublic 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();//实现了 RunnableThreadProxy threadProxy = new ThreadProxy(tiger);threadProxy.start();} }//线程代理类 , 模拟了一个极简的Thread类 class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxyprivate Runnable target = null;//属性,类型是 Runnable@Overridepublic 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@Overridepublic 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@Overridepublic 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;@Overridepublic 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);//1t.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());//1t.interrupt();//当执行到这里,就会中断 t线程的休眠.} }class T extends Thread { //自定义的线程类@Overridepublic 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,便抛出一个异常,之后便执行catchThread.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 {@Overridepublic 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 {@Overridepublic 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;//让多个线程共享 ticketNumprivate 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}}@Overridepublic 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();// 保证多线程,共享一个对象,这里使用staticstatic Object o2 = new Object();boolean flag;public DeadLockDemo(boolean flag) {//构造器this.flag = flag;}@Overridepublic void run() {//下面业务逻辑的分析//1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁//2. 如果线程A 得不到 o2 对象锁,就会Blocked//3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁//4. 如果线程B 得不到 o1 对象锁,就会Blockedif (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()
来控制线程,方法不在推荐使用
-