多线程

1. 线程简介

  • 程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
  • 进程:是执行程序的一次执行过正,是一个动态的概念。是系统资源分配的单元
  • 线程:通常在一个进程中可以包含若干个线程,至少有一个,不然没有存在的意义。线程是CPU调度和执行的单位

核心概念:

  1. 线程就是独立的执行路径;
  2. 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  3. main()称之为主线程,为系统的入口,用于执行整个程序
  4. 在一个进程中,如果开辟了多个线程,线程的运行有调度器安排调整,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
  5. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  6. 线程会带来额外的开销,如cpu调度时间,并发控制开销
  7. 每个线程在自己的工作内存交互,内存控制不当就会造成数据不一致

2. 线程实现(重点)

三种方式:

2.1 继承Thread类(重点)

不建议使用:避免OOP单继承局限性

package com.cao.Thread;
//创建线程方式一:继承Thread类,重写run方法,调用start开启线程
//总结:注意,线程开启不一定立即执行,是由cpu调度的
public class TestThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 2000; i++) {
            System.out.println("i am seeing code------>" + i);
        }
    }

    public static void main(String[] args) {
        // main方法,主线程
        // 创建一个线程对象
        TestThread1 t1 = new TestThread1();
        // 调用start()开启线程
        t1.start();
        for (int i = 0; i < 2000; i++) {
            System.out.println("我在看代码---->"+i);
        }
    }
}

2.2 实现Runnable接口(重点)

推荐使用:避免单继承局限性,灵活方便,方便同一个对象被对各线程使用

package com.cao.Thread;
//创建线程方式二:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread2 implements Runnable{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 2000; i++) {
            System.out.println("i am seeing the code------>" + i);
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现对象
        TestThread2 t2 = new TestThread2();

        //创建线程对象,通过线程对象来开启线程,代理
        Thread tt2 = new Thread(t2);
        tt2.start();
        for (int i = 0; i < 2000; i++) {
            System.out.println("我在看代码---->" + i);
        }
    }
}

龟兔赛跑

package com.cao.Thread;

/**
 * 龟兔赛跑
 */
public class Race implements Runnable{
    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            /*
            if(Thread.currentThread().getName().equals("rabbit")){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }*/
            boolean flag = isGameOver(i);
            if(flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }

    // 判断游戏是否结束
    private Boolean isGameOver(int steps){
        // 判断是否有胜利者
        if(winner != null){
            return true;
        }else{
            if(steps >= 100){
                winner = Thread.currentThread().getName();
                System.out.println("the winner is "+winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"turtle").start();
        new Thread(race,"rabbit").start();
    }
}

2.3 实现Callable接口(了解)

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submint(t1);
  6. 获取结果:boolean r1 = result1.get();
  7. 关闭服务:ser.shurdownNow();

3. Lambda表达式

3.1 为什么要用lambda表达式

  • 避免匿名内部类定义过多
  • 可以让代码更加简洁
  • 去点无意义diamagnetic,只留下核心逻辑

3.2 函数式接口

  • 定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口

    //此为一个函数式接口Runnable
    public interface Runnable{
        public abstract void run();//只有一个抽象方法
    }
    
  • lambda方法是函数式编程

3.3 推导lambda表达式

package com.cao.lambda;

/**
 * 推导lambda表达式
 */
public class LambdaTest {
    // 3. 静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("i love study lambda222");
        }
    }
    public static void main(String[] args) {
        ILike i = new LIke();
        i.lambda();
        i = new Like2();
        i.lambda();


        // 4. 局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i love study lambda333");
            }
        }
        i = new Like3();
        i.lambda();


        // 5. 匿名内部类,没有类的名称,必须借助接口或者父类
        i = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i love study lambda444");
            }
        };
        i.lambda();


        // 6. 用lambda简化,i是ILike接口的引用,该接口只有一个抽象方法,所以直接省略冗余代码直接实现方法
        i = ()->{
            System.out.println("i love study lambda555");
        };
        i.lambda();
    }
}

// 1. 定义一个函数式接口
interface ILike{
    public void lambda();
}

// 2. 实现类
class LIke implements ILike{
    @Override
    public void lambda() {
        System.out.println("i love study lambda111");
    }
}

简化过程

// 原版lambda
lv = (String times)->{
    System.out.println("i love you " + times + "times");
};
lv.love("three thousand");

// 简化1.参数类型
lv = (times)->{
    System.out.println("i love you " + times + "times");
};
lv.love("three thousand");

// 简化2.括号
lv = times->{
    System.out.println("i love you " + times + "times");
};
lv.love("three thousand");

// 简化3.大括号
lv = times->System.out.println("i love you " + times + "times");
lv.love("three thousand");

3.4 总结

  1. 多个参数也可以简化参数类型,需要都去掉参数类型且加括号
  2. 必须是函数式接口:接口里面只能有一个方法
  3. Lambda表达式只能有一行代码的情况下才能简化成为一行,否则需要用代码块

4. 线程状态

4.1 线程的5种状态

  1. new(新生状态):Thread t = new Thread()线程一旦创建就进入到了新生状态
  2. 就绪状态:当调用start()方法,线程立即进入就绪状态,但不意味着立即执行调度
  3. 运行状态:进入运行状态,线程才真正执行线程体的代码块
  4. 阻塞状态:当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待CPU调度执行
  5. dead(死亡状态):线程中断或者结束,一旦进入死亡状态,就不能再次启动

4.2 线程的几种方法

Method introduction
setPriority(int newPriority) 更改线程的优先级
static void sleep(long mills) 让当前线程休眠指定的毫秒数的时长
void join() 等待该线程终止
static void yield() 在听当前正在执行的线程,并执行其他线程
void interrupt() 中断线程,别使用这个方式
boolean isAlive() 判断线程是否处于活动状态

4.3 线程停止

  • 不推荐使用JDK提供的stop()、destroy()方法 【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,当flag = false,则终止线程运行

4.3 线程休眠 sleep

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

4.4 线程礼让 yield

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功!看CPU心情
package com.cao.Thread;
// 测试礼让线程
// 礼让不一定成功,看cpu心情
public class TestYield {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"t1").start();
        new Thread(myThread,"t2").start();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

4.3 Join

  • Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞

  • 可以想象成插队

    package com.cao.Thread;
    // 测试join方法
    public class TestJoin implements Runnable{
        public static void main(String[] args) throws InterruptedException {
            TestJoin tj = new TestJoin();
            Thread thread = new Thread(tj);
            thread.start();
            for (int i = 0; i < 1000; i++) {
                if(i == 200){
                    thread.join();  //插队
                }
                System.out.println("main" + i);
            }
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程VIP来了"+i);
            }
        }
    }
    
    

4.4 Thread.getState()

获取当前线程的状态:

  • NEW
  • RUNNABLE
  • TIMED_WAITING
  • BLOCKED
  • TERMINATED

4.5 线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围从1~10

    • Thread.Min_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式改变或获取优先级

    • getPriority()

    • setPriority(int xxx)

    • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度

    • 优先级的设定建议在start()调度前

      example

    package com.cao.Thread;
    // 测试线程的优先级
    public class TestPriority implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
        }
    
        public static void main(String[] args) {
            // 主线程优先级
            System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
    
            TestPriority priority = new TestPriority();
            Thread t1 = new Thread(priority);
            Thread t2 = new Thread(priority);
            Thread t3 = new Thread(priority);
            Thread t4 = new Thread(priority);
            Thread t5 = new Thread(priority);
            Thread t6 = new Thread(priority);
    
            // 先设置优先级,再启动
            t1.start();
    
            t2.setPriority(1);
            t2.start();
    
            t3.setPriority(4);
            t3.start();
    
            t4.setPriority(Thread.MAX_PRIORITY);//10
            t4.start();
    
        }
    }
    
    

4.6 守护线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如:后台记录操作日志,监控内存,垃圾回收等待
package com.cao.Thread;
// 测试守护线程
// 上帝守护你
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        // 默认是false表示是用户线程,正常的县城都是用户线程
        thread.setDaemon(true);
        thread.start();//上帝守护线程启动

        new Thread(you).start();// you用户线程启动
    }
}

class God implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("上帝保佑着你");
        }
    }
}

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你一生都开心的活着");
        }
        System.out.println("Goodbye World!");
    }
}

5. 线程同步(重点)

5.1 概念

  • 现实生活中,会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,最天然的方法就是排队,一个一个来
  • 并发:同一个对象被多个线程同时操作
  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。此时我们需要用到线程同步,它其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

5.2 线程同步

  • Synchronized由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多项成竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
  • 死锁

    多个线程各自站有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题 。

    死锁产生必须同时满足如下的4个条件:

    1. 互斥条件:一个资源每次只能被一个进程使用
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

    以上4个条件只要有一个不成立,则死锁不会发生。

  • Lock锁

    • 从JDK5.0开始,Java提供了更强大的线程同步机制---通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当

    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

    • ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

      package lock;
      
      import com.sun.xml.internal.bind.v2.model.annotation.RuntimeAnnotationReader;
      
      import java.util.concurrent.locks.ReentrantLock;
      
      public class TestLock {
          public static void main(String[] args) {
              MyLock MyLock = new MyLock();
              new Thread(MyLock).start();
              new Thread(MyLock).start();
              new Thread(MyLock).start();
          }
      }
      
      class MyLock implements Runnable{
          int ticketNum = 10;
          private final ReentrantLock lock = new ReentrantLock();
          @Override
          public void run() {
               while(true){
                   try{
                       lock.lock();
                       if(ticketNum > 0){
                           System.out.println(ticketNum --);
                           Thread.sleep(1000);
                       }else {
                           break;
                       }
                   }catch (InterruptedException e) {
                       e.printStackTrace();
                   } finally {
                      lock.unlock();
                   }
               }
          }
      }
      
      

5.3 Synchronized 与 Lock

synchronized Lock
隐式锁,出了作用域自动释放 显式锁,手动开启与关闭
有代码块锁和方法锁 只有代码块锁
使用后,JVM花费较少时间调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

优先级使用顺序:

​ Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

6. 线程通信问题

  • Java提供了几个方法解决线程之间的通信问题

    方法名 作用
    wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
    wait(long timeout) 指定等待的毫秒数
    notify() 唤醒一个处于等待状态的线程
    notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
  • 应用场景:生产者和消费者问题

    • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
    • 如果仓库中没有产品,则生产者将产品放入长裤,否则停止生产并等待,知道仓库中的产品被消费者取走为止
    • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
  • 解决方式1:并发写作模型"生产者/消费者模式"--->管程法

    • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
    • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
    • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

    生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

  • 线程池

    背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

    思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具(共享单车)。

    好处:

    1. 提高响应速度(减少了创建新线程的时间)
    2. 降低资源消耗(重复利用线程池中线程看,不需要每次都创建)
    3. 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTIme:线程没有任务时最多保持多长时间后会终止
posted on 2021-02-01 11:50  caoshikui  阅读(72)  评论(0)    收藏  举报