Java多线程学习 (超详细总结)
Java多线程学习
- 一、概要
- 二、 JAVA 线程实现/创建方式
- 2.1 继承Thread 类
- 2.2 实现 Runnable 接口
- 2.3 Thread和Runnable的区别
- 2.4 总结
- 三、线程生命(状态)
- 3.1 解释:
- 3.2 新建状态(NEW):
- 3.3 就绪状态(RUNNABLE):
- 3.4 阻塞状态(BLOCKED):
- 3.4.1 等待阻塞(o.wait->等待对列)
- 3.4.2 同步阻塞(lock->锁池)
- 3.4.3 其他阻塞(sleep/join)
- 3.5 线程死亡(DEAD)
- 四、线程调度
- 4.1 调整线程优先级:
- 4.2 线程睡眠:
- 4.3 线程等待:
- 4.4 线程让步:
- 4.5 线程加入:
- 4.6 线程唤醒:
- 五、线程基本方法
- 六、sleep 与 wait区别
- 七、start 与 run 区别
- 八、JAVA 后台线程
- 8.1 定义:
- 8.2 优先级:
- 8.3 设置:
- 8.4 线程则是 JVM 级别的
- 8.5 example:
- 8.6 生命周期:
- 九、线程数据传递
- 9.1、通过构造方法传递数据
- 9.2、通过变量和方法传递数据
- 9.3、通过回调函数传递数据
- 十、Java锁
- 10.1 乐观锁
- 10.2 悲观锁
- 10.3 自旋锁
- 10.4 Synchronized 同步锁
- 10.4.1 Synchronized 作用范围
- 10.4.2 Synchronized 核心组件
- 了解更多Java锁相关知识
一、概要
首先了解一下什么是线程:
java.lang.Thread类中有这样的明确定义:线程是程序中执行的线程,Java虚拟机允许程序同时运行多个执行线程(即多线程)。
多线程的用处
- 更高的运行效率,——并行;
- 多线程是模块化的编程模型;
- 与进程相比,线程的创建和切换开销更小;
- 通信方便;
- 能简化程序的结构,便于理解和维护;更高的资源利用率。
二、 JAVA 线程实现/创建方式
2.1 继承Thread 类
示例:
package com.zking.test02;//方法一继承 Thread 类
public class MyThread extends Thread{@Overridepublic void run() {System.out.println("MyThread.run()");}public static void main(String[] args) {MyThread myThread1 = new MyThread();myThread1.start();} }
2.2 实现 Runnable 接口
package com.zking.test02.t02;/*** 实现 Runnable 接口* @author 借我丹青妙笔* @date 2022年1月16日*/
public class MyThread extends OtherClass implements Runnable {public String name;public MyThread(String name) {this.name = name;}public void run() {System.out.println(name+"线程.run()");}public static void main(String[] args) {//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例://方法一MyThread myThread = new MyThread("A"); Thread thread = new Thread(myThread); //方法二Thread thread2 = new Thread(new MyThread("B"));thread.start();thread2.start();//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用/*target.run()public void run() { if (target != null) { target.run(); } }*/}}
2.3 Thread和Runnable的区别
- 类继承Thread,则不适合资源共享。
- 但是如果实现了Runable接口的话,则很容易的实现资源共享。
2.4 总结
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
三、线程生命(状态)
3.1 解释:
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
3.2 新建状态(NEW):
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
3.3 就绪状态(RUNNABLE):
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
3.4 阻塞状态(BLOCKED):
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。
阻塞的情况分三种:
3.4.1 等待阻塞(o.wait->等待对列)
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。 (wait会释放持有的锁)
3.4.2 同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
3.4.3 其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。(注意:sleep不会释放持有的锁)
3.5 线程死亡(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
1.run()或 call()方法执行完成,线程正常结束。
异常结束
2.线程抛出一个未捕获的 Exception 或 Error。
调用 stop
3.直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
四、线程调度
4.1 调整线程优先级:
Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
4.2 线程睡眠:
Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
4.3 线程等待:
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
4.4 线程让步:
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
4.5 线程加入:
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
4.6 线程唤醒:
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
五、线程基本方法
- sleep(): 强迫一个线程睡眠N毫秒。
- isAlive(): 判断一个线程是否存活。
- join(): 等待线程终止。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
- setName(): 为线程设置一个名称。
- wait(): 强迫一个线程等待。
- notify(): 通知一个线程继续运行。
- notifyAll():唤醒在此对象监视器上等待的所有线程。
- setPriority(): 设置一个线程的优先级。
六、sleep 与 wait区别
- 对于sleep()方法是属于Thread 类中的。而wait()方法则是属于Object 类中的
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
- 在调用 sleep()方法的过程中,线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
七、start 与 run 区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 run 方法运行结束,此线程终止。然后 CPU 再调度其它线程。
八、JAVA 后台线程
8.1 定义:
守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
8.2 优先级:
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
8.3 设置:
通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
8.4 线程则是 JVM 级别的
以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程 依旧是活跃的。
8.5 example:
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
8.6 生命周期:
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出
九、线程数据传递
在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。
9.1、通过构造方法传递数据
在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:
package com.zking.test02.t03;/*** 构造函数传值* @author 借我丹青妙笔* @date 2022年1月16日*/
public class MyThread1 extends Thread {private String name;public MyThread1(String name) {this.name = name;}public void run() {System.out.println("hello " + name);}public static void main(String[] args) {Thread thread = new MyThread1("world");thread.start();}
}
由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。
9.2、通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:
package com.zking.test02.t03;/*** 通过变量和方法传递数据 * @author 借我丹青妙笔* @date 2022年1月16日*/
public class MyThread2 implements Runnable {private String name;public void setName(String name) {this.name = name;}public void run() {System.out.println("hello " + name);}public static void main(String[] args) {MyThread2 myThread = new MyThread2();myThread.setName("world");Thread thread = new Thread(myThread);thread.start();}
}
9.3、通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。
package com.zking.test02.t03;/*** 通过变量和方法传递数据* * @author 借我丹青妙笔* @date 2022年1月16日*/
public class MyThread3 extends Thread {private Work work;public MyThread3(Work work) {this.work = work;}public void run() {java.util.Random random = new java.util.Random();Data data = new Data();int n1 = random.nextInt(1000);int n2 = random.nextInt(2000);int n3 = random.nextInt(3000);Integer[] numbers = { n1, n2, n3 };work.process(data, numbers); // 使用回调函数System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" + String.valueOf(n3) + "=" + data.value);}public static void main(String[] args) {Thread thread = new MyThread3(new Work());thread.start();}
}class Data {public int value = 0;
}class Work {public void process(Data data, Integer[] numbers) {for (int n : numbers) {data.value += n;}}
}
十、Java锁
10.1 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数 据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。 java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
10.2 悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
10.3 自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗
10.4 Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
10.4.1 Synchronized 作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中
10.4.2 Synchronized 核心组件
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到所资源的线程被称为 Owner;
- !Owner:当前释放锁的线程。
了解更多Java锁相关知识
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
声明:学习记录,欢迎留言讨论。
摘录:生活有一百种过法,别人的故事再好,始终容不下你。活成什么样子,自己决定。 ——张皓宸
作者:借我丹青妙笔
Java多线程学习 (超详细总结)相关推荐
- Java 多线程(超详细)
多线程学习思路:为什么学习线程?为了解决CPU利用率问题,提高CPU利用率. => 什么是进程?什么是线程? => 怎么创建线程?有哪几种方式?有什么特点? => 分别怎么启动线程? ...
- 一起学JAVA 反射学习(超详细)
1 什么是反射? Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说"自审",也有称作"自省&quo ...
- 转:Java多线程学习(总结很详细!!!)
Java多线程学习(总结很详细!!!) 此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢? 本文主要讲java中多线程 ...
- java多线程学习-java.util.concurrent详解
http://janeky.iteye.com/category/124727 java多线程学习-java.util.concurrent详解(一) Latch/Barrier 博客分类: java ...
- JAVA 正则表达式 (超详细,转)
转 JAVA 正则表达式 (超详细,转) 2015年03月25日 10:27:57 阅读数:1514 在Sun的Java JDK 1.40版本中,Java自带了支持正则表达式的包,本文就抛砖引玉地介绍 ...
- 【转】Java 多线程学习
原网址:https://www.cnblogs.com/yjd_hycf_space/p/7526608.html Java多线程学习(总结很详细!!!) 此文只能说是java多线程的一个入门,其实J ...
- Java多线程学习处理高并发问题
在程序的应用程序中,用户或请求的数量达到一定数量,并且无法避免并发请求.由于对接口的每次调用都必须在返回时终止,因此,如果接口的业务相对复杂,则可能会有多个用户.调用接口时,该用户将冻结. 以下内容将 ...
- Java多线程学习(二)synchronized关键字(1)
转载请备注地址: https://blog.csdn.net/qq_34337272/article/details/79655194 Java多线程学习(二)将分为两篇文章介绍synchronize ...
- java多线程学习笔记。
java多线程学习笔记 线程的优缺点: 多线程的好处: 充分利用多处理核心,提高资源的利用率和吞吐量. 提高接口的响应效率,异步系统工作. 线程的风险: 安全危险(竞争条件):什么坏事都没有发生.在没 ...
最新文章
- 双系统gazebo闪退_记录Ubuntu16.04下PX4联合Gazebo仿真时遇到的问题与解决方法
- 黄聪:SQL server 2005高可用性之----数据库镜像
- Indicator Weather 13.06 发布 增加 Kelvin 支持
- MySQL事务效率测试
- 有三AI小程序上线,把你的代码show给世界
- Nginx和lvs在负载均衡方面的对比
- php列目录设置密码,PHP输入密码并列出目录文件生成超链接代码
- 企业私有云部署im,视频服务
- 普大喜奔 | Azure 免费送网站SSL证书啦!
- 【iVX 初级工程师培训教程 10篇文拿证】01 了解 iVX 完成新年贺卡
- java selenium (十二) 操作弹出窗口
- 怎么看待MYSQL的性能
- 吐血解决python中文写入文件问题
- Protues 8.8 SP1 无需破解 可用直装版 指路
- 超效率dea matlab,求高人帮忙关于超效率DEA模型编程
- 硬核!教你三种方法,实现微信自定义修改地区!
- 6个方法帮交互设计师与上下游顺畅合作
- 年龄在线计算机,年龄计算器在线计算2021 抖音上很火的精确年龄岁数计算器查询 - 房贷计算器...
- 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活 HDU - 2191(特殊01背包)
- Activiti流程定义缓存源码分析5-流程缓存
热门文章
- 利用微波技术,亚瑟士要为用户打造独一无二定制化跑鞋
- Uc的个人中心很奇葩
- 同一个SqlHelper链接不同的数据库
- 图像隐写分析——生成含密图像 Stego Image
- 疫情下思乡的心理学调查问卷
- MobaXterm连接远程服务器,使用Linux命令行上传下载文件
- AffordaceNet属性学习网络踩坑记录(二)
- android textview layoutparams,Android动态设置布局的LayoutParams属性总遇到造型异常
- Scrapy源码学习-Middleware
- 实时全局光照(Real-time Global Illumination)与Reflective Shadow Maps(RSM)