【JavaSE】多线程基础学习笔记

多线程基础

-线程相关概念

  • 程序(Program)

    是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码

  • 进程

    1. 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
    2. 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
  • 什么是线程

    1. 线程是由进程创建的,是进程的一个实体
    2. 一个进程可以拥有多个线程
    3. 线程也可以创建线程
  • 其他相关概念

    1. 单线程:同一个时刻,只允许执行一个线程
    2. 多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
    3. 并发:同一个时刻,多个任务交替执行,造成一种"貌似同时"的错觉,简单说,单核cpu实现的多任务就是并发
    4. 并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。

-线程基本使用

  • 创建线程的两种方式

    1. 继承Thread 类,重写 run方法
    2. 实现Runnable接口,重写run方法
  • 线程应用案例1-继承Thread类

    java 复制代码
    package com.xijie.useThread;
    
    /**
     * 请编写程序,开启一个线程,该线程每隔1秒。在控制台输出"喵喵, 我是小猫咪"
     * 对上题改进:当输出5次 喵喵, 我是小猫咪,结束该线程
     * 使用JConsole 监控线程执行情况,并画出程序示意图!:指令jconsole
     */
    public class ThreadUse {
        public static void main(String[] args) throws InterruptedException {
            Cat cat = new Cat();
            //开始运行线程
            cat.start();
    
            //获取主线程名称
            System.out.println("主线程开始运行,主线程名为:"+Thread.currentThread().getName());
    
            //主线程内循环输出40次
            for (int i = 0; i < 40; i++) {
                System.out.println("线程名:"+Thread.currentThread().getName()+" 循环次数:"+(i+1));
                Thread.sleep(1000);
            }
        }
    }
    
    //继承Thread或实现Runnable以实现线程类
    class Cat extends Thread{
        @Override
        public void run() {
            //super.run();
            //计数器,到达5次就停止
            int count=0;
            //死循环,一直执行
            while(true){
                //输出内容、线程名、循环次数
                System.out.println("喵喵, 我是小猫咪 第"+(++count)+"次循环,线程名:"+Thread.currentThread().getName());
                //输出60次就停止线程
                if(count>=60){
                    break;
                }
                //休眠1秒,需处理异常
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    start方法底层:start方法调用start0方法。start0方法由JVM获取对应的实现,底层一般由c或c++语言实现,创建线程完毕后再由线程执行run方法。

  • 线程应用案例2-实现Runable接口

    说明

    1. java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了。
    2. java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程

    案例代码

    java 复制代码
    package com.xijie.useRunnable;
    
    /**
     * 请编写程序,该程序可以每隔1秒。在控制台输出"hi!",当输出10次后,自动退出。
     * 请使用实现Runnable接口的方式实现。
     * 写一个演示代理模式实现线程的类
     */
    public class UseRunnable {
        public static void main(String[] args) {
            HiTalker talker = new HiTalker();
            //使用Runnable开辟线程
            Thread t1 = new Thread(talker);
            t1.start();
    
            //模拟用代理模式实现线程(没有另开线程,因为开辟线程需要调用操作系统的方法实现)
            ProxyThread proxy = new ProxyThread(talker);
            proxy.start();
        }
    }
    
    class HiTalker implements Runnable {
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("hi!"+Thread.currentThread().getName());
                try {
                    if(i == 9)
                        break;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /**
     * 模拟用代理模式实现线程(没有另开线程,因为开辟线程需要调用操作系统的方法实现)
     */
    class ProxyThread implements Runnable {
        private Runnable target;
    
        public ProxyThread(Runnable target) {
            this.target = target;
        }
    
        @Override
        public void run() {
            //如果target已初始化,就在线程中执行run方法
            if(target != null) {
                target.run();
            }
        }
    
        /**
        开始线程的方法
         */
        public void start(){
            this.start0();
        }
    
        /**
         * 在Thread类中JVM会重写start0方法调用操作系统中开辟线程的方法
         */
        private void start0() {
            System.out.println("调用操作系统方法创建线程");
            this.run();
        }
    }
  • 线程应用案例-多线程执行

    java 复制代码
    package com.xijie.multiThread;
    
    /**
     * 用实现3个Runnable接口的进程,一个每隔1秒输出hi,10次,一个每隔2秒输出,4次nice,一个每隔3秒输出,3次wtf
     */
    public class MultiThread {
        public static void main(String[] args) {
            Thread t1 = new Thread(new MyThread(10,"hi",1000));
            Thread t2 = new Thread(new MyThread(4,"nice",2000));
            Thread t3 = new Thread(new MyThread(3,"wtf",3000));
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    //每隔1秒输出,10次hi
    class MyThread implements Runnable {
        int count;
        String str;
        int delay;
    
        public MyThread(int count, String str, int delay) {
            this.count = count;
            this.str = str;
            this.delay = delay;
        }
    
        @Override
        public void run() {
            int count=0;
            while(true){
                System.out.println(this.str);
                if(++count==this.count)
                    break;
                try {
                    Thread.sleep(this.delay);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
  • 线程的理解

    线程像工厂里的多条流水线,各自忙着不同工序,共享机器设备(内存等资源)。CPU 是调度员,快速切换让它们 "同时" 运转,协作完成程序的复杂任务,互不干扰又紧密配合。

-继承Thread与实现Runnable的区别

  1. 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口

  2. 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制

  3. 售票系统超卖问题演示(竞态条件问题)

    java 复制代码
    package com.xijie.sellerProblem;
    
    /**
     * 创建三个售票员线程,演示超卖问题
     */
    public class SellerProblem {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new SellerRunnable("jack"));
            Thread thread2 = new Thread(new SellerRunnable("mary"));
            Thread thread3 = new Thread(new SellerRunnable("mike"));
    
            //执行后余票很可能会小于0,出现超卖问题
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    
    class SellerRunnable implements Runnable {
        private static int ticketNum;
    
        private String sellerName;
    
        static{
            ticketNum = 100;
        }
    
        public SellerRunnable(String sellerName) {
            this.sellerName = sellerName;
        }
    
        @Override
        public void run() {
            while(true){
                //售票
                System.out.println("售票员"+sellerName+"卖出一张票,余票为"+(--ticketNum)+"张");
                //若无票则下班
                if(ticketNum <= 0){
                    break;
                }
                //延迟10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

-线程终止

  • 基本说明

    1. 当线程完成任务后,会自动退出,
    2. 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式
  • 使用通知变量通知线程结束的案例

    java 复制代码
    package com.xijie.threadEnd;
    
    public class ThreadEnd {
        public static void main(String[] args) throws InterruptedException {
            MyThread myThread = new MyThread();
            myThread.start();
    
            Thread.sleep(2000);
    
            System.out.println("结束执行");
            myThread.stopRunning();
        }
    }
    
    class MyThread extends Thread{
        //设置结束通知变量
        private boolean loop=true;
    
        @Override
        public void run() {
            int count=0;
            while(loop){
                System.out.println("线程执行"+(++count)+"次");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    
        public void stopRunning(){
            loop=false;
        }
    }

-线程常用方法

  • 常用方法

    1. setName //设置线程名称,使之与参数 name 相同
    2. getName //返回该线程的名称
    3. start //使该线程开始执行;Java 虚拟机底层调用该线程的 start0 方法
    4. run //调用线程对象 run 方法:
    5. setPriority //更改线程的优先级
    6. getPriority //获取线程的优先级
    7. sleep//在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
    8. interrupt //中断线程
    9. yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
    10. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
  • 注意事项和细节

    1. start 底层会创建新的线程,调用run,run 就是一个简单的方法调用,不会启动新线程线程
    2. 优先级的范围:Thread.MAX_PRIORITY/MIN_PRIORITY/NORM_PRIORITY
    3. interrupt,中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程
    4. sleep:线程的静态方法,使当前线程休眠
  • 线程中断和插队练习

    java 复制代码
    package com.xijie.interruptJoin;
    
    /**
     * 主线程开辟一个子线程
     * 子线程输出一次"我要睡觉了",随后休眠10秒,主线共输出10次hi!,间隔1秒,在主线程输出3次hi后,中断子线程,随后子线程输出3次"wtf"后,主线程再输出剩余的hi
     */
    public class InterruptJoin {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("hi!"+Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(i==2){
                    thread.interrupt();
                    thread.join();
                }
            }
    
            System.out.println("我嗨完了"+Thread.currentThread().getName());
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("我要睡觉了"+Thread.currentThread().getName());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("我醒了"+Thread.currentThread().getName());
            }
    
            for (int i = 0; i < 3; i++) {
                System.out.println("wtf!"+Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
  • 用户线程和守护线程

    1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
    2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
    3. 常见的守护线程:垃圾回收机制

    守护线程案例:

    java 复制代码
    package com.xijie.daemon;
    
    /**
     * 演示守护线程
     * 主线程输出10次"王宝强在辛苦工作",间隔200ms
     * 子线程一直输出"宋喆和马蓉在愉快的聊天",间隔100ms,直到主线程结束
     */
    public class ShowDaemon {
        public static void main(String[] args) {
            Dating dating = new Dating();
            Thread thread = new Thread(dating);
            //设置为守护线程
            thread.setDaemon(true);
            thread.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("王宝强在辛苦工作"+(i+1));
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    class Dating implements Runnable{
        @Override
        public void run() {
            for (;;){
                System.out.println("宋喆和马蓉在愉快的聊天");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

-线程的生命周期

  • JDK中用Thread.State枚举表示了线程的几种状态(6+1)
    1. NEW
      尚未启动的线程处于此状态,
    2. RUNNABLE(在内部包含READY和RUNNING两个状态)
      在Java虚拟机中执行的线程处于此状态。
    3. BLOCKED
      被阻塞等待监视器锁定的线程(等待进入同步代码块)处于此状态。
    4. WAITING
      正在等待另一个线程执行特定动作的线程处于此状态,
    5. TIMED WAITING正在等待另一个线程执行动作达到指定等待时间的线程(例如sleep的线程)处于此状态,
    6. TERMINATED
      已退出的线程处于此状态

-Synchronized

  • 线程同步机制

    1. 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
    2. 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.
  • 同步具体方法-Synchronized

    1. 同步代码块

      java 复制代码
      synchronized(对象){//得到对象的锁,才能操作同步代码
          // 需要被同步代码;
      }
    2. synchronized还可以放在方法声明中,表示整个方法-为同步方法

      java 复制代码
      public synchronized void m (String name){
      //需要被同步的代码
      }
    3. 如何理解:就好像 某小伙伴上厕所前先把门关上(上锁),完事后再出来(解锁),那么其它小伙伴就可在使用厕所了

    4. 使用synchronized 解决售票问题

      java 复制代码
      package com.xijie.synchronized_;
      
      /**
       * 创建三个售票员线程,演示超卖问题
       */
      public class SynchronizedTickedSelling {
          public static void main(String[] args) {
              SellerRunnable sellerRunnable=new SellerRunnable("MMM");
              new Thread(sellerRunnable).start();
              new Thread(sellerRunnable).start();
              new Thread(sellerRunnable).start();
              new Thread(sellerRunnable).start();
          }
      }
      
      class SellerRunnable implements Runnable {
          private static int ticketNum;
          private String sellerName;
      
          static{
              ticketNum = 200;
          }
      
          public SellerRunnable(String sellerName) {
              this.sellerName = sellerName;
          }
      
          @Override
          public void run() {
              while(this.ticketNum > 0) {
                  sellATicket();
              }
              System.out.println("票卖完了,大家下班了");
          }
      
          private synchronized void sellATicket(){
              if(ticketNum <= 0){
                  System.out.println("售票员"+Thread.currentThread().getName()+"出售失败,余票为"+(ticketNum)+"张");
                  return;
              }
              System.out.println("售票员"+Thread.currentThread().getName()+"卖出一张票,余票为"+(--ticketNum)+"张");
              //延迟10毫秒
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
      }

-互斥锁

  • 基本介绍

    1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
    2. 每个对象都对应于一个可称为"互斥锁"的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
    3. 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
    4. 同步的局限性:导致程序的执行效率要降低
    5. 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
    6. 同步方法(静态的)的锁为当前类本身。
  • 注意事项和细节

    1. 同步方法如果没有使用static修饰:默认锁对象为this

    2. 如果方法使用static修饰,默认锁对象:当前类.class

    3. 实现的落地步骤:

      需要先分析上锁的代码

      选择同步代码块或同步方法

      要求多个线程的锁对象为同一个即可!

-线程死锁

  • 基本介绍

    多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生.

-释放锁

  • 下面操作会释放锁

    1. 当前线程的同步方法、同步代码块执行结束

      案例:上厕所,完事出来

    2. 当前线程在同步代码块、同步方法中遇到break、return经理叫他修改bug,不得已出来

      案例:没有正常的完事,

    3. 当前线程在同步代码块同步方法中出现了未处理的Error或Exception,导致异常结束

      案例:没有正常的完事,发现忘带纸,不得已出来

    4. 当前线程在同步代码块、同步方法中执行了线程对象的wait0)方法,当前线程暂停,并释放锁。

      案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去

  • 下面操作不会释放锁

    1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁

      案例:上厕所,太困了,在坑位上眯了一会

    2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起该线程不会释放锁。

      提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用

-本章练习

java 复制代码
package com.xijie.homework;

import java.util.Random;
import java.util.Scanner;

/**
 * 在main方法中启动两个线程
 * 第1个线程循环随机打印100以内的整数
 * 直到第2个线程从键盘读取了"Q"命令,
 */
public class Homework01 {
    public static void main(String[] args) {
        LotteryRunnable lotteryRunnable1=new LotteryRunnable(null,1);
        Thread thread1 = new Thread(lotteryRunnable1);
        thread1.start();

        LotteryRunnable lotteryRunnable2=new LotteryRunnable(lotteryRunnable1,2);
        Thread thread2 = new Thread(lotteryRunnable2);
        thread2.start();
    }
}

//抽奖机
class LotteryRunnable implements Runnable {
    LotteryRunnable lotteryRunnable;
    private boolean loop=true;
    private int workType;

    public LotteryRunnable(LotteryRunnable lotteryRunnable,int workType) {
        this.lotteryRunnable=lotteryRunnable;
        this.workType = workType;
    }

    @Override
    public void run() {
        if (workType == 1) {
            NumberTyper();
        } else if (workType == 2) {
            if(lotteryRunnable==null){
                System.out.println("传参错误");
                return;
            }
            qGetter();
        }
    }

    //循环打印
    private void NumberTyper() {
        while(loop){
            int number = new Random().nextInt(100);
            System.out.println(number);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //接收q
    private void qGetter(){
        while(true){
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入q停止摇奖机");
            String str=scanner.next().trim();
            if(str.equals("q")){
                lotteryRunnable.loop=false;
                break;
            }
        }
    }
}
java 复制代码
package com.xijie.homework;

/**
 * 有2个用户分别从同一个卡上取钱(总额:10000)
 * 每次都取1000,当余额不足时,就不能取款了
 * 不能出现超取现象 =》 线程同步问题,
 */
public class Homework02 {
    public static void main(String[] args) {
        BankAccount bankAccount=new BankAccount(10000);
        AccountOwner accountOwner1=new AccountOwner("小明",bankAccount,10);
        AccountOwner accountOwner2=new AccountOwner("小李",bankAccount,10);

        Thread t1=new Thread(accountOwner1);
        Thread t2=new Thread(accountOwner2);
        t1.start();
        t2.start();
    }
}

class BankAccount{
    private double money;

    BankAccount(double money){
        this.money = money;
    }

    public double getMoney() {
        return money;
    }

    public boolean withdraw(double money){
        if(money <= this.money){
            this.money -= money;
            return true;
        }else{
            return false;
        }
    }
}

class AccountOwner implements Runnable{
    private String name;
    //对象锁
    private BankAccount account;
    private int withdrawInter;

    AccountOwner(String name,BankAccount account,int withdrawInter){
        this.name = name;
        this.account = account;
        this.withdrawInter = withdrawInter;
    }

    @Override
    public void run() {
        boolean flag = false;
        while(true){
            //使用对象锁实现同步
            synchronized (account){
                flag=account.withdraw(1000);
                if(!flag){
                    System.out.println(name+"取款失败,余额不足,余额还有"+account.getMoney());
                    break;
                }
                System.out.println(name+"取款1000成功,余额还有"+account.getMoney());
            }

            try {
                Thread.sleep(withdrawInter);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
相关推荐
404.Not Found21 分钟前
Day50 Python打卡训练营
python·深度学习·机器学习
GISDance26 分钟前
26考研 专业课 百度网盘夸克网盘
学习·考研
Jacob023428 分钟前
告别Excel地狱!用 PostgreSQL + ServBay 搭建跨境电商WMS数据中枢
数据库·python
斯文by累1 小时前
Python环境搭建
开发语言·python
USER_A0011 小时前
高等数学(下)题型笔记(八)空间解析几何与向量代数
笔记·高等数学
YKPG2 小时前
C++学习-入门到精通【17】自定义的模板化数据结构
数据结构·c++·学习
156992 小时前
大语言模型原理与书生大模型提示词工程实践-学习笔记
笔记·学习·语言模型
.似水2 小时前
Python requests
开发语言·python
不会飞的鲨鱼2 小时前
FastMoss 国际电商Tiktok数据分析 JS 逆向 | MD5加密
javascript·python·数据挖掘·数据分析
tanyyinyu2 小时前
Python列表:高效灵活的数据存储与操作指南
开发语言·windows·python