多线程基础学习

多线程基础学习

进程
  • 在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

  • 进程和线程的关系:一个进程可以包含一个或多个线程,但至少会有一个线程。

创建一个线程
  • 方法一:继承Thread.类,重写run()方法,调用start开启线程

    • public class Demo04 extends Thread{//继承Thread类
          @Override
          public void run() {//重写run方法
              for (int i = 0; i < 5; i++) {
                  System.out.println("线程里的方法:"+i);
              }
          }
      
          public static void main(String[] args) {
              Demo04 demo04 = new Demo04();
              demo04.start();//用start方法开启线程
              //demo04.run();//这个就不会另外起一个线程
              System.out.println("主线程中的方法");
          }//start方法会产生主方法和另一个线程的方法同时执行,而直接调用run方法就会按原来顺序从上往下执行
      }
      
  • 方法二(推荐):实现Runnable接口,创建线程对象,通过线程对象来开启我们的线程,代理

    • public class Demo05 implements Runnable{//实现Runnable接口
          @Override
          public void run() {
              for (int i = 0; i < 5; i++) {
                  System.out.println("线程里的方法"+i);
              }
          }
      
          public static void main(String[] args) {
              Demo05 demo05 = new Demo05();//创建线程对象
              new Thread(demo05).start();//通过线程对象来开启我们的线程
              //重载方法:new Thread(实现Runnable接口的对象,线程的命名);
              System.out.println("主线程中的方法");
          }
      }
      
线程并发
  • 并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。强调通过很小的时间片的切换对将程序交替执行,假象的并行效果。

  • 并行:两个以上事件(或线程)在同一时刻发生,没有时间片的交换,跑在不同的CPU资源上,一般是多核

  • 抢票实例:

    • public class Demo06 implements Runnable{
          private  int TicketNum =10;//共同的抢票
          @Override
          public void run() {
              while (TicketNum>0){
                  System.out.println(Thread.currentThread().getName()+"抢到了第"+(TicketNum--)+"张票");//Thread.currentThread().getName()可以获取线程的名称;
              }
          }
      
          public static void main(String[] args) {
              Demo06 Ticket = new Demo06();
              new Thread(Ticket,"1号线程").start();
              new Thread(Ticket,"2号线程").start();
              new Thread(Ticket,"3号线程").start();
          }
      }//有可能会出现抢同一张票的情况
      //发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
      
静态代理
  • 优点:可以在不修改目标对象的前提下扩展目标对象的功能

  • 操作步骤

    1. 先定义一个接口(有相印的方法)
    2. 定义真实类和代理类,都要实现这个接口
    3. 代理类中创建一个接口的属性,创建这个属性的构造函数
    4. 主函数中代理类的构造函数传入一个实现接口的类/实例就可以实现代理的拓展功能
    • public interface Marry {//定义接口,
          void HappyMarry();//接口中的方法
      }
      class You implements Marry {//真实的类实现接口
          @Override
          public void HappyMarry() {
              System.out.println("终于结婚了");
          }
      }
      class WeddingHouse implements Marry {//代理类,实现接口
          private  Marry target;//定义一个接口的属性
          public WeddingHouse(Marry target) {
              this.target = target;
          }
          @Override
          public void HappyMarry() {
              before();//对原来接口的扩充
              target.HappyMarry();//原来接口的方法
              after();//对原来接口的扩充
          }
          private void after() {
              System.out.println("处理后事");
          }
          private void before() {
              System.out.println("邀请嘉宾");
          }
      }
      //主函数中实现
      public static void main(String[] args) {
          //第一种,直接创建一个代理类的对象,并且在构造函数中传入实现接口的真实对象
              WeddingHouse weddingHouse = new WeddingHouse(new You());
              weddingHouse.HappyMarry();
          //第二种,直接调用隐士对象,就相当是new Thread(实现Runnable接口的对象).start();
              new WeddingHouse(new You()).HappyMarry();
          //第三种,直接采用匿名内部类,直接进行输出代理拓展的内容
       	    new WeddingHouse(new Marry() {
                  @Override
                  public void HappyMarry() {
                      System.out.println("我也要结婚啦");//这里没有用真实的类,根据自己需求实现想要的功能
                  }
              }).HappyMarry();
      }
      
lambda表达式
  • 函数式接口的定义:
    任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

    public interface Runnable{
    	public abstract void run();
    }
    

    对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。

  • 实现:创建一个类,实现接口,通过接口创建属性就可以新建一个实现接口类的对象,调用实现的方法,或者式lambda表达式

    • interface Love {
          void iLove();
      }
      class You implements Love {//实现了接口
          @Override
          public void iLove() {
              System.out.println("i love you ");//重写方法
          }
      }
       public static void main(String[] args) {
              Love love = new You();//新建一个接口的属性,创建一个实现类的对象
              love.iLove();//通过实例来调用方法
              Love love1 = new Love() {
                  @Override
                  public void iLove() {
                      System.out.println("i love you 1");
                  }
              };//匿名内部类,不用实现类就可以直接通过接口进行方法的内部实现
              love1.iLove();
              Love love2 = () -> 
                      System.out.println("i love you 2");//lambda表达式对上面的简化, 实质是一样的。不需要写new ,也不需要写方法名(因为只有一个);只有实现方法体
              love2.iLove();
      }
      //如果有参数: Love love2 = (参数) -> System.out.println("i love you 2");
      
线程的五大状态
  • image-20220623150006422
  • 含义:在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了(死亡的线程无法再次start)。因此,Java线程的状态有以下几种:

    • New:新创建的线程,尚未执行;
    • Runnable:运行中的线程,正在执行run()方法的Java代码;
    • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
    • Waiting:运行中的线程,因为某些操作在等待中;
    • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
    • Terminated:线程已终止,因为run()方法执行完毕。

    用一个状态转移图表示如下:

             ┌─────────────┐
             │     New     │
             └─────────────┘
                    │
                    ▼
    ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
     ┌─────────────┐ ┌─────────────┐
    ││  Runnable   │ │   Blocked   ││
     └─────────────┘ └─────────────┘
    │┌─────────────┐ ┌─────────────┐│
     │   Waiting   │ │Timed Waiting│
    │└─────────────┘ └─────────────┘│
     ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                    │
                    ▼
             ┌─────────────┐
             │ Terminated  │
             └─────────────┘
    

    当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

    线程终止的原因有:

    • 线程正常终止:run()方法执行到return语句返回;
    • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
    • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

    一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

    线程方法:
    方法说明
    setPriority(int newPriority)更改线程的优先级
    static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休
    void join()等待该线程终止
    static void yield()暂停当前正在执行的线程对象,并执行其他
    线程
    void interrupt()中断线程,别用这个方式
    boolean isAlive测试线程是否处于活动状态
    线程停止的方法:
    • 建议线程正常停止—>利用次数,不建议死循环。

    • 建议使用标志位—>设置一个标志位

    • 不要使用stop或者destroy等过时的方法

      • class ThreadStop implements Runnable {//线程类
            private boolean flag = true;//提供线程体中的标识
            public void run() {
                while (flag) {//线程使用标识
                    System.out.println("Thread is running");
                }
            }
            public void stop() {//提供外部方法停止
                flag = false;
            }
            public static void main(String[] args) {
                ThreadStop threadStop = new ThreadStop();
                new Thread(threadStop).start();
                for (int i = 0; i < 999; i++) {
                    if (i == 900)
                        threadStop.stop();//调用线程类提供的公有方法
                    System.out.println("main is running");
                }
            }
        }
        
    线程休眠:
    • 作用:
      • 模拟网络延时,放大问题的发生性->(排查线程不安全)
    • 每个对象都有一个锁,sleep不会释放锁
    线程礼让:
    • 作用:
      • 礼让线程,让当前正在执行的线程暂停,但不阻塞->(礼让一下,再竞争一次)
      • 将线程从运行状态转为就绪状态
      • 让CPU重新调度,礼让不一定成功!看CPU心情
    • yield也不会释放锁
  • 线程强制执行jion

    • 可以将jion想象成插队,插主线程,很霸道,但是对于两个并行的线程无法直接插队
  • 线程的优先级

    • 如果有些比较重要的代码,需要先跑,可以设置优先级,虽然设置优先级并不一定会一定在前面先执行,但是至少他所占有的机会会变大一点

    • 默认为5,最高的优先级为10,最低为1,超范围报错

    • 线程优先级的两个方法

      • setPriority(级别);//设置线程优先级
        getPriority();//获取线程的优先级
        
    守护线程
    • 守护线程就像餐厅的服务员,而普通的用户线程就像是顾客,如果顾客都走光了,那么守护线程存在的意义也没有了,自然也会结束

    • 作用

      • 常见的做法,就是将守护线程用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。
      • 咦,按照这个解释,那么大多数 JVM 线程都是守护线程。
    • 守护线程的方法:

      • setDaemon(true)//设置守护线程,注意,一定要在start方法之前设置守护线程;
        isDaemon()//判断是否是守护线程
        
线程同步
  • 形成条件:队列+锁

    重点:synchronized到底锁的是谁
  • 下面用一个实例来证明

  • class Date {
        public void func1() {//普通方法
            try {
                Thread.sleep(3000);//休眠3秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("1...");
        }
    
        public void func2() {//普通方法
            System.out.println("2...");
        }
    }
    public static void main(String[] args) {
            Date date = new Date();
            new Thread(date::func1, "A").start();//lambda表达式启动线程
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            new Thread(date::func2,"B").start();//lambda表达式启动线程
    }
    //结果:三秒后输出2...再过1秒后输出1...
    //原因:两个都是新起的线程,没有相关的同步性,按程序顺序执行
    //2...
    //1...
    
    1. 锁方法
      • 普通方法:谁调用就锁谁

        • public synchronized void func1() {//把上面的修改为同步方法
          public synchronized void func2() {//把上面的修改为同步方法
          //结果:过了三秒同时输出,结果是先1...后2...
          //原因,锁了这个方法,谁来调用就锁哪个对象,这里只有一个对象,date,所以先启动A线程,然后过了sleep3秒(不放锁),主程序的sleep1秒对这里没有影响,直接就已经包含在sleep的3秒内了,因线程同步,三秒钟后,等A的锁释放,B立刻拿锁,输出
          //1...
          //2...
          
        • image-20220623195402761
        • public synchronized void func1() {//把上面的修改为同步方法
          public void func2() {//普通方法
          //结果:1秒后输出2...再过2秒输出1...
          //原因:B线程不是同步的方法,直接就是两个线程在跑,
          
        • image-20220623195937379
        • public static void main(String[] args) {//新建两个Date()对象
                 Date date1 = new Date();
                 Date date2 = new Date();
                 new Thread(date1::func1, "A").start();
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
                 new Thread(date2::func2,"B").start();
          }
           public synchronized void func1() {//把上面的修改为同步方法
           public synchronized void func2() {//把上面的修改为同步方法
           //结果:1秒后输出2...再过2秒输出1...
           //原因:,直接就是两个线程在跑,没有同步争夺资源
          
      • 静态方法:锁定的是类

        • public static void main(String[] args) {
                  Date date1 = new Date();
                  Date date2 = new Date();
                  new Thread(() -> {
                      date1.func1();
                  }, "A").start();
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
                  new Thread(() -> {
                      date2.func2();
                  }, "B").start();
          }
          public static synchronized void func1() {//变成了静态方法
          public static synchronized void func2() {//变成了静态方法
          //结果:过了三秒同时输出,结果是先1...后2...
          //原因:虽然date1与date2是两个不同的对象,但是因为静态同步方法锁的是这个类,两个对象都是来自同一个类,所以存在线程同步的关系。
          
    2. 修饰代码块:
      • 可以锁各种东西,放什么锁什么

        • class Date2 {
              public void func() {
                  System.out.println("start...");
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
                  System.out.println("end...");
              }
          }
          public static void main(String[] args) {
                  Date2 date2 = new Date2();
                  for (int i = 0; i < 5; i++) {
                      new Thread(() ->
                      {
                          date2.func();
                      }).start();//创建5个线程
                  }
          }
          //结果:直接是5个start,1秒后输出5end
          //原因:没有任何锁,直接是5个线程
          
        • //只改变成同步代码块
          public void func() {
                  synchronized (this) {//改成代码同步
                      System.out.println("start...");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      System.out.println("end...");
                  }
          }
          //结果:在排队,交替输出start和end
          //原因:这里this是这个Date2的对象,只有一个date2对象,变成了线程同步
          
        • public static void main(String[] args) {
                 for (int i = 0; i < 5; i++) {
                     new Thread(() ->
                     {
                         Date2 date2 = new Date2();//只改变每次创建的对象
                         date2.func();
                     }).start();
                 }
          }
          //结果:在排队,交替输出start和end
          //原因:虽然同步代码块修饰了这个类创建的对象,但这时候对象创建放在了循环里,相当每次创建不同的对象,那么每次调用func方法就是直接开启新线程,没有线程同同步
          
        • synchronized (Date2.class) {//改成代码同步锁这个类
          //结果:在排队,交替输出start和end
          //原因:锁的是这个类,虽然创建的是5个不同的对象,但是都是同一个类,产生了线程同步
          
  • 死锁:

    多个线程互相抱着对方需要的资源,然后形成僵持即一个线程可以获取一个锁后,再继续获取另一个锁。

    • public void add(int m) {
          synchronized(lockA) { // 获得lockA的锁
              this.value += m;
              synchronized(lockB) { // 获得lockB的锁
                  this.another += m;
              } // 释放lockB的锁
          } // 释放lockA的锁
      }
      
      public void dec(int m) {
          synchronized(lockB) { // 获得lockB的锁
              this.another -= m;
              synchronized(lockA) { // 获得lockA的锁
                  this.value -= m;
              } // 释放lockA的锁
          } // 释放lockB的锁
      }
      
    • 解决方案:一个线程可以获取一个锁后,再继续获取另一个锁。

    • 那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致,一次只获得一把锁,避免一个线程同时获得两把锁

  • lock
    • lock是显示锁,只能锁代码块

    • 好处:用lockjvm将花费较少的时间调度线程,性能更好,可拓展性高

    • class TestLock implements Runnable {
          private int ticket = 100;
          private  final ReentrantLock lock = new ReentrantLock();//可重入的锁,一定要记住这个方法,通过他的lOck和unlock方法实现同步
          @Override
          public void run() {
              while (true) {
                  try {
                      lock.lock();//在需要变化的地方设置锁
                      if (ticket <= 0) {
                          return;
                      } else {
                          System.out.println(ticket--);
                      }
                  } catch (RuntimeException e) {
                      throw new RuntimeException(e);
                  } finally {
                      lock.unlock();//一般放在finally块里释放锁
                  }
              }
          }
      }
      
线程协作
  • 生产者消费者模型具体来讲,就是在一个系统中,存在生产者和消费者两种角色,他们通过内存缓冲区进行通信,生产者生产消费者需要的资料,消费者把资料做成产品。(是一种问题,不是设计模式)

    再具体一点:

    • 生产者生产数据到缓冲区中,消费者从缓冲区中取数据。
    • 如果缓冲区已经满了,则生产者线程阻塞。
    • 如果缓冲区为空,那么消费者线程阻塞。
  • 通过wait()和notify()方法,采用 阻塞队列 方式实现生产者消费者模式

    管程法:
    • class Producer extends Thread{
          //生产者
          SynContainer Container;
          public Producer( SynContainer Container){
              //通过与消费者相同构造函数实现同一个线程
              this.Container =Container;
          }
      
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  Container.push(new Chicken(i));
                  System.out.println("生产力"+i+"鸡");
              }
          }
      }
      class Consumer extends Thread{
          //消费者
          SynContainer Container;
          public Consumer( SynContainer Container){
            //通过与生产者相同构造函数实现同一个线程
              this.Container =Container;
          }
      
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  Chicken pop = Container.pop();
                  System.out.println("消费类"+pop.id+"只鸡");
              }
          }
      }
      class Chicken {//生产的对象
          int id;//对象id
          public Chicken(int id) {
              this.id = id;
          }
      }
      class SynContainer {
          //同步容器,锁就是锁他,通过他来实现中间的桥梁,使两个线程跑在这个上,变成并发
          Chicken[] chickens = new Chicken[10];//定义容器大小
          int count = 0;//记录生成的数
      
          public synchronized void push(Chicken chicken) {
              if (count == chickens.length) {
                  try {
                      wait();//容器满了就会调用wait方法释放锁,让消费者先行
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
              chickens[count++] = chicken;
              this.notifyAll();//释放锁
          }
      
          public synchronized Chicken pop() {
              if (count == 0) {
                  try {
                      wait();//没有消费对象的时候,就会wait释放锁,让生产者先行
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
              count--;
              Chicken chicken = chickens[count];
              this.notifyAll();//释放锁
              return chicken;
          }
      }
      public static void main(String[] args) {
              SynContainer synContainer = new SynContainer();
              Producer producer = new Producer(synContainer);
              Consumer consumer = new Consumer(synContainer);
              producer.start();
              consumer.start();
      
      }
      //结果:一个线程里,生产者与消费者正常是交错执行,如果满足了wait的条件,就释放了锁,给另外一方执行
      
线程池
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
    可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

  • 好处:

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(.…)
  • 用法:

     public static void main(String[] args) {
            //1.创建服务,创建线程池
            //newFixedThreadPool参数为:线程池大小
            ExecutorService service = Executors.newFixedThreadPool(10);
            //执行
            service.execute(new 实现了Runnable接口的类);
           service.shutdown();
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值