Java基础之多线程&JUC全面学习笔记

本是古典 何须时尚 2023-09-29 18:57 54阅读 0赞

目录

  • 初识多线程
  • 多线程的实现方式
  • 常见的成员方法
  • 线程安全的问题
  • 死锁
  • 生产者和消费者
  • 线程池
    • 自定义线程池

初识多线程

什么是多线程?
线程
线程是操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位。

简单理解:应用软件中互相独立,可以同时运行的功能

那什么是进程呢?
进程
进程是程序的基本执行实体,比如,打开任务管理器,可以看到其中有很多的进程

  1. 多线程作用:提高效率

多线程的两个概念

并发
并发:在同一时刻,有多个指令在单个CPU上交替执行

并行
并行:在同一时刻,有多个指令在多个CPU上同时执行


多线程的实现方式

①继承Thread类的方式进行实现

多线程的第一种启动方式:

1.自己定义一个类继承Thread
2.重写run方法
3.创建子类的对象,并启动线程

  1. public class MyThread extends Thread{
  2. @Override
  3. public void run() {
  4. //书写线程要执行代码
  5. for (int i = e; i < 100; i++) {
  6. system.out.println(getName( ) + "Helloworld" );
  7. }
  8. }
  9. }
  10. public static void main( String[] args) {
  11. MyThread t1 = new MyThread();
  12. MyThread t2 = new MyThread() ;
  13. t1.setName("线程1");
  14. t2.setName("线程2");
  15. t1.start();
  16. t2.start();
  17. }

②实现Runnable接口的方式进行实现

多线程的第二种启动方式:
1.自己定义一个类实现Runnable接口2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread类的对象,并开启线程

  1. public static void main(String[] args) {
  2. //创建实现Runnable接口的类的对象
  3. RunnableInf run = new RunnableInf();
  4. //创建线程对象
  5. Thread t1 = new Thread(run);
  6. Thread t2 = new Thread(run);
  7. //给线程设置名字
  8. t1.setName("线程一");
  9. t2.setName("线程二");
  10. //开启线程
  11. t1.start();
  12. t2.start();
  13. }
  14. public class RunnableInf implements Runnable{
  15. @Override
  16. public void run() {
  17. //书写线程执行代码
  18. for (int i = 0; i < 100; i++){
  19. System.out.println(Thread.currentThread().getName()+"hello world");
  20. }
  21. }
  22. }

③利用Callable接口和Future接口方式实现

多线程的第三种实现方式:

特点:可以获取到多线程运行的结果

步骤如下
1.创建一个类MyCallable实现callable接口
2.重写call (是有返回值的,表示多线程运行的结果)
3.创建MyCallable的对象(表示多线程要执行的任务)
4.创建FutureTask的对象(作用管理多线程运行的结果)
5.创建Thread类的对象,并启动(表示线程)

  1. public static void main(String[] args) throws ExecutionException, InterruptedException {
  2. //创建Mycallable的对象(表示多线程要执行的任务)
  3. MyCallable mc = new MyCallable();
  4. //创建FutureTask的对象(作用管理多线程运行的结果)
  5. FutureTask<Integer> ft = new FutureTask<>(mc ) ;
  6. //创建线程的对象
  7. Thread t1 = new Thread(ft);
  8. //启动线程
  9. t1.start();
  10. //获取线程运行结果
  11. Integer result = ft.get();
  12. System.out.println(result);
  13. }
  14. public class MyCallable implements Callable<Integer> {
  15. @Override
  16. public Integer call() throws Exception {
  17. //求100之间的和
  18. int sum = 0;
  19. for (int i = 0; i <= 100; i++ ){
  20. sum += i;
  21. }
  22. return sum;
  23. }
  24. }

三种实现方式的对比

在这里插入图片描述


常见的成员方法

在这里插入图片描述

方法细节点

void setName( string name)
设置线程的名字(构造方法也可以设置名字)
细节:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始的)
2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置(记得继承Thread类的构造方法,快捷键Alt+Ins)


static Thread currentThread( )
获取当前线程的对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码,在以前,我们写的所有的代码,其实都是运行在main线程当中


static void sleep( long time)
让线程休眠指定的时间,单位为毫秒
细节:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2、方法的参数:就表示睡眠的时间,单位毫秒;1秒= 1000毫秒
3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码


线程调度两种方式

  • 抢占式调度 最大特点是随机,根据优先级来抢占cpu资源,优先级越高,抢占概率越大
  • 非抢占式调度 特点:你一次,我一次,有规律
    在这里插入图片描述

优先级是0档到10档,默认优先级都是5
从Thread源码中可以看到

在这里插入图片描述


final void setDaemon( boolean on)
设置为守护线程细节:
当其他的非守护线程执行完毕之后,守护线程会陆续结束,不是立刻结束

通俗易懂:照下图案例
当女神线程结束了,那么备胎也没有存在的必要了

女神线程
在这里插入图片描述
备胎线程
在这里插入图片描述

main方法中
在这里插入图片描述

最后结果就是当线程1循环10次结束后,守护线程陆续结束(不会循环完,也不会立刻停止)


守护线程使用场景
比如QQ聊天,传输文件
在这里插入图片描述


出让线程解释

在这里插入图片描述


插入线程演示

  1. public static void main(String[] args) throws InterruptedException {
  2. //创建实现Runnable接口的类的对象
  3. RunnableInf run = new RunnableInf();
  4. //创建线程对象
  5. Thread t1 = new Thread(run);
  6. //Thread t2 = new Thread(run);
  7. //给线程设置名字
  8. t1.setName("线程一");
  9. //开启线程
  10. t1.start();
  11. //把t1线程插入到当前线程之前
  12. t1.join(); //这个代码运行在哪个线程上,就插入在哪个线程前
  13. for (int i = 0; i < 10; i++){
  14. System.out.println("main线程"+i);
  15. }
  16. }

线程的生命周期

在这里插入图片描述


线程安全的问题

由于线程抢占cpu资源是随机的,所以在执行业务操作时,可能同一个业务,多条线程并行执行,而造成数据错乱或丢失;比如买票的经典案例,有可能当一个线程进去执行买票逻辑时,另一个线程也进去了,结果导致卖出了两张同样编号的票。经典问题就是超卖,一票多卖,有票没卖。

解决方法自然就是给执行的业务代码加上锁,保证原子性


方法之一:同步代码块

把操作共享数据的代码锁起来

在这里插入图片描述

  1. 特点1:锁默认打卉,有一个线程进去了,锁自动关闭
  2. 特点2:里面的代码全部执行完毕,线程出来,锁自动打开

同步代码块使用如下,卖100张票的案例

  1. public class MyThread extends Thread{
  2. //表示这个类的所有对象共享ticket数据
  3. static int ticket = 0;
  4. //创建锁对象,一定要唯一
  5. static Object object = new Object();
  6. @Override
  7. public void run() {
  8. while (true){
  9. //同步代码块
  10. synchronized (object){
  11. if (ticket < 100){
  12. ticket++;
  13. System.out.println(getName()+"正在卖第" +ticket +"张票");
  14. }else {
  15. break;
  16. }
  17. }
  18. }
  19. }
  20. }
  21. public class Treaddemo {
  22. public static void main(String[] args) {
  23. MyThread t1 = new MyThread();
  24. MyThread t2 = new MyThread();
  25. t1.setName("窗口1");
  26. t2.setName("窗口2");
  27. t1.start();
  28. t2.start();
  29. }
  30. }

方法之二:同步方法
在这里插入图片描述
就是把synchronized关键字加到方法上

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己指定
非静态: this
静态: 当前类的字节码文件对象


卖100张票的案例

  1. public class MyRunnable implements Runnable{
  2. int ticket = 0;//由于MyRunnalbe方法只new了一个对象,可以不用把ticket设置为共享变量
  3. @Override
  4. public void run() {
  5. while (true){
  6. if (method()) break;
  7. }
  8. }
  9. private synchronized boolean method() {
  10. synchronized (MyRunnable.class){
  11. if ( ticket == 100 ){
  12. return true;
  13. }else {
  14. try {
  15. Thread.sleep(100);
  16. } catch (InterruptedException e) {
  17. throw new RuntimeException(e);
  18. }
  19. ticket++;
  20. System.out.println(Thread.currentThread().getName()+ "正在卖第"+ ticket +"张票");
  21. }
  22. }
  23. return false;
  24. }
  25. }
  26. public class Threaddemo {
  27. public static void main(String[] args) {
  28. MyRunnable mr = new MyRunnable();
  29. Thread t1 = new Thread(mr);
  30. Thread t2 = new Thread(mr);
  31. Thread t3 = new Thread(mr);
  32. t1.setName("窗口一");
  33. t2.setName("窗口二");
  34. t3.setName("窗口三");
  35. t1.start();
  36. t2.start();
  37. t3.start();
  38. }
  39. }

方法之三:同步方法

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,手动上锁、手动释放锁

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法

  1. void lock():获得锁
  2. void unlock():释放锁

Lock是接口不能直接实例化,通常采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法

ReentrantLock():创建一个ReentrantLock的实例

还是以买100张票为案例,使用锁的方式代码如下

  1. public class LockThread extends Thread{
  2. static int ticket = 0;
  3. static Lock lock = new ReentrantLock();
  4. public LockThread() {
  5. }
  6. public LockThread(String name) {
  7. super(name);
  8. }
  9. @Override
  10. public void run() {
  11. while (true){
  12. //这次换用锁的方式
  13. lock.lock();//上锁
  14. try {
  15. if (ticket < 100){
  16. Thread.sleep(100);
  17. ticket++;
  18. System.out.println(getName()+"正在卖第" +ticket +"张票");
  19. }else {
  20. break;
  21. }
  22. } catch (InterruptedException e) {
  23. throw new RuntimeException(e);
  24. } finally {
  25. lock.unlock();
  26. }
  27. }
  28. }
  29. }
  30. public class MainThread {
  31. public static void main(String[] args) {
  32. LockThread t1 = new LockThread("窗口一");
  33. LockThread t2 = new LockThread("窗口二");
  34. LockThread t3 = new LockThread("窗口三");
  35. t1.start();
  36. t2.start();
  37. t3.start();
  38. }
  39. }

死锁

通俗点说就是锁嵌套了,线程1拿了A锁,就在线程1拿A锁时,线程2入了B锁,而线程1中间的操作还要再入B锁才能执行业务并结束;而线程2中间操作还要再入A锁才能执行业务并结束。最终二者都在等待对方释放锁,造成了死锁情况


生产者和消费者

等待唤醒机制
等待唤醒机制会让两个线程轮流执行,标准的你一次我一次

在这里插入图片描述

细节点:notify()是随机唤醒一个线程,不容易控制,一般使用的是notifyAll()方法

下面是经典的消费者和生产者案例代码,消费者是食客,生产者是厨师,中间控制是桌子,控制线程执行

中间控制者桌子

  1. public class Desk {
  2. /**
  3. * 控制消费者和生产者的执行
  4. */
  5. //是否有面条 0:没有面条 1:有面条
  6. public static int foodFlag = 0;
  7. //总个数,也就是消费者需要的总个数
  8. public static int count = 10;
  9. //锁对象
  10. public static Object lock = new Object();
  11. }

食客

  1. public class Eater extends Thread{
  2. /**
  3. * 1.循环
  4. * 2.同步代码块
  5. * 3.判断共享数据是否到了末尾(到了末尾执行的逻辑)
  6. * 4.判断共享数据是否到了末尾(没到末尾执行的逻辑)
  7. */
  8. @Override
  9. public void run() {
  10. while (true){
  11. if (Desk.count == 0){
  12. break;
  13. }
  14. synchronized (Desk.lock){
  15. if (Desk.foodFlag == 0){
  16. //先看是否有产品,没有就等待
  17. try {
  18. Desk.lock.wait();//让当前线程跟锁绑定
  19. } catch (InterruptedException e) {
  20. throw new RuntimeException(e);
  21. }
  22. }else {
  23. Desk.count--;
  24. System.out.println("消费者正在消费产品,还需要消费数量:"+ Desk.count);
  25. //消费完唤醒生产者继续做
  26. Desk.lock.notifyAll();
  27. //修改桌子状态
  28. Desk.foodFlag = 0;
  29. }
  30. }
  31. }
  32. }
  33. }

厨师

  1. public class Cooker extends Thread{
  2. @Override
  3. public void run() {
  4. while (true){
  5. if (Desk.count == 0){
  6. break;
  7. }
  8. synchronized (Desk.lock){
  9. if (Desk.foodFlag == 1){
  10. try {
  11. Desk.lock.wait();
  12. } catch (InterruptedException e) {
  13. throw new RuntimeException(e);
  14. }
  15. }else {
  16. //修改桌上食物状态
  17. Desk.foodFlag = 1;
  18. System.out.println("生产者生产了面条");
  19. //唤醒消费者
  20. Desk.lock.notifyAll();
  21. }
  22. }
  23. }
  24. }
  25. }

main方法

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. Eater e = new Eater();
  4. Cooker c = new Cooker();
  5. e.setName("消费者");
  6. c.setName("生产者");
  7. e.start();
  8. c.start();
  9. }
  10. }

注意点
Desk. lock. wait(); //让当前线程跟锁进行绑定
Desk.Lock.notifyAll(); //唤醒跟这把锁绑定的所有线程
这里之所以用锁对象调用,是避免notifyAll()方法唤醒所有线程(包括方法外的比如系统线程等),用对象调用,可以指明唤醒的是哪个线程
(这里用Desk类中的lock对象调用,是应为lock对象是唯一的,只是在Desk类中new了一次)


线程池

在这里插入图片描述
用线程池作为容器存放线程
①创建一个池子,池子中是空的

②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可

③但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
在这里插入图片描述

在这里插入图片描述


自定义线程池

线程池工作流程,它会有核心线程数量,临时线程数量,队列长度,空闲时间这个几个因素影响决定

当任务数量超过核心线程数量时,就会让多余任务在队列排队
当任务数量超过核心线程数量,且队列排满的情况,才会使用临时线程来处理任务
当任务数量超过核心线程数量,最大排队数量以及临时线程数量的总和,全负载时,多余任务会根据线程池的拒绝策略来丢弃或处理

临时线程的空闲时间在超过线程池初始化规定的时间就会销毁
而核心线程只有在销毁线程池时才会销毁


创建线程池会发现有7个参数

在这里插入图片描述

最后任务拒绝策略有以下几种

在这里插入图片描述

创建自定义线程池代码如下

在这里插入图片描述


最后感谢您的阅览,希望这篇文章能为您解除疑惑

发表评论

表情:
评论列表 (有 0 条评论,54人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java 线学习笔记 线互斥

    许多线程共享同一数据,这种情况在现实的生活中也是经常发生的,比如火车站的火车票售票系统。火车票售票系统是一个常年运行的系统,为了满足乘客的需求,我们不能只设一个窗口,必须设很多