石一歌的多线程笔记

Java多线程

基本概念

  • 进程
    • 进程就是执行程序的一次执行过程, 它是一个动态的概念, 式系统资源分配的单位
    • 通常再一个进程中可以包含若干个线程, 当然一个进程中至少有一个线程, 不然没有存在的意义, 线程是 CPU 调度和执行的单位
  • 线程
    • 线程就是独立的执行路径
    • 在程序运行时, 即使没有自己创建线程, 后台也会有多个线程, 比如主线程, GC 线程
    • main() 称之为主线程, 为系统的入口, 用于执行整个程序
    • 在一个进程中, 如果开辟了多个线程, 线程的运行是由调度器安排调度的, 调度器是与操作系统紧密相关的, 先后顺序是不能人为干预的
    • 对同一份资源操作时 mm 会存在资源抢夺的问题, 需要加入并发控制
    • 线程会带来额外的开销, 如 CPU 调度时间, 并发控制开销
    • 每个线程在自己的工作内存交互, 内存控制不当会造成数据不一致
  • 多线程(单核并发,多核并行
    • 一个进程中有多个线程
    • 为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待
    • 进程之间不能共享数据,线程可以
    • 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小
    • Java语言内置了多线程功能支持,简化了java多线程编程

线程创建

Thread

不建议使用,避免oop单继承局限性

  • 继承自 Thread 类,

  • 重写 run 方法,

  • 创建实例调用 start 方法

    public class ThreadTest extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程方法被调用");
        }
        public static void main(String[] args) {
            new ThreadTest().start();
        }
    }
    

Runnable

灵活方便,方便同一个对象被多个线程使用

  • 实现 Runnable 接口

  • 实现 run 方法

  • 创建 Thread 时作为参数传入, 调用 start 方法(代理)

    public class RunnableTest implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程方法被调用");
        }
        public static void main(String[] args) {
            new Thread(new RunnableTest()).start();
        }
    }
    

Callable

功能丰富,可以定义返回值,可以抛出异常

  • 实现 Callable 接口

  • 重写 call 方法

  • 创建执行服务,提交执行,获取结果,关闭服务

    public class CallableTest implements Callable<Boolean> {
        @Override
        public Boolean call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "线程方法被调用");
            return true;
        }
        public static void main(String[] args) {
            CallableTest callable = new CallableTest();
            //创建执行服务
            ExecutorService service = Executors.newFixedThreadPool(1);
            //提交执行
            Future<Boolean> result = service.submit(callable);
            //获取结果
            boolean isTrue = result.get();
            //关闭服务
            service.shutdownNow();
        }
    }
    

线程状态

  • 创建状态
  • 就绪状态
  • 阻塞状态
  • 运行状态
  • 死亡状态

QQ截图20220124151928

QQ截图20220124221201

线程方法

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

停止线程

  • 不推荐使用JDK提供的stop()、destroy()方法。【已废弃】

  • 推荐线程自己停止下来

  • 建议使用一个标志位进行终止变量当flag=false,则终止线程运行。

    public class RunnableTest implements Runnable {
        private boolean flag = true;
        @Override
        public void run() {
            int i = 0;
            while ( flag){
                System.out.println( "run. . . .Thread"+i++);
            }
        }
        public void stop( ){
            this.flag = false;
        }
    }
    

线程休眠

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

线程礼让

  • yield() 礼让线程, 让当前正在执行的线程暂停, 但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让 CPU 重新调度, 礼让不一定成功

线程强制执行

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

线程状态观测

  • Thread.State

线程状态:线程可以处于以下状态之一:

  • NEW:尚未启动的线程处于此状态。
  • RUNNABLE:在 Java 虚拟机中执行的线程处于此状态。
  • BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
  • WAITING:正在等待另一个线程执行特定动作的线程处于此状态。
  • TERMINATED:已退出的线程处于此状态。

线程优先级

  • java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程; 线程调度器按照优先级决定该调度哪个线程来执行
  • 线程的优先级用数字来表示, 范围从 1~10
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY= 10
    • Thread.NORM_PRIORITY = 5
  • 使用 getPriority() 和 setPriority() 来获取或改变优先级

守护线程

  • thread.setDeamon(true)设置守护线程
  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如后台记录操作日志, 监控内存, 垃圾回收

线程同步

  • 处理多线程问题时, 多个线程访问同一个对象, 并且某些线程还想修改这个对象, 这时候我们就需要线程同步, 线程同步其实是一种等待机制, 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面的线程使用完毕, 下一个线程在使用
  • 由于同一进程的多个线程共享同一块存储空间, 在带来方便的同时, 也带来了访问的冲突问题, 为了保证数据在方法中被访问时的正确性, 在访问时加入锁机制 syncronized, 当一个线程获得对象的排他锁, 独占资源, 其他线程必须等待, 使用后释放锁即可, 存在一下问题
    • 一个线程持有锁会导致其他所有需要此锁的进程挂起
    • 在多线程竞争的情况下, 加锁, 释放锁会导致比较多的上下文切换和调度延时, 引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置, 引起性能问题

死锁

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

  • 死锁产生的条件

    • 互斥条件: 一个资源每次只能被一个进程使用

    • 请求保持条件: 一个进程因请求资源而阻塞时. 对以获得的资源保持不放

    • 不剥夺条件: 进程已获得的资源, 在未使用完之前, 不能强行剥夺

    • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系

synchronized

由于我们可以通过关键字 private 关键字来保证数据对象只能被方法访问,所以我们只要针对方法提出一套机制就可以保证线程安全,这套机制就是 synchronized 关键字,

包括两种方法:synchronized 方法和 synchronized 块.

  • 同步方法

    • public synchronized void method(int args){
      }
      
    • synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。

    • 缺陷:若将一个大的方法申明为 synchronized 将会影响效率。读资源无需加锁。

  • 同步块

    • synchronized(obj){}
      
    • obj 称之为同步监视器

      • obj 可以是任何对象, 但是推荐使用共享资源作为同步监视器
      • 同步方法中无需指定同步监视器, 因为同步方法中的同步监视器就是 this, 就是这个对象本身, 或者是 class
    • 同步监视器的执行过程

      • 第一个线程访问, 锁定同步监视器, 执行其中的代码
      • 第二个线程访问, 发现同步监视器被锁定, 无法访问
      • 第一个线程访问完毕, 解锁同步监视器
      • 第二个线程访问, 发现同步监视器没有锁, 然后锁定并访问

Lock

  • JDK1.5 开始, java 提供了更为强大的线程同步机制——通过显示定义同步锁对象来实现同步, 同步锁使用 lock 对象来充当

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

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

  • 使用

    class TestLock implements Runnable {
        private final ReentrantLock lock = new ReentrantLock();//定义Lock锁
        @Override
        public void run() {
            try {
                lock.lock(); //加锁
                //需要保证线程安全的代码
            } finally {
                lock.unlock();//解锁
            }
        }
    }
    

Lock和synchronized

Lock synchronized
格式 显式锁 (手动开启和关闭, 别忘记关闭锁) 隐式锁, 出了作用域自动释放
类型 代码块锁 代码块锁和方法锁
优点 使用 lock 锁, JVM 将花费较少的时间来调度线程, 性能更好, 并且具有更好的扩展性 (提供更多的子类) 可以直接给方法加锁
  • 优先级
    • Lock > 同步代码块 > 同步方法

线程通信

  • 生产者和消费者问题

    • 假设仓库中只能存放一件产品, 生产者将生产出来的产品放入仓库, 消费者将仓库产品取走消费

    • 如果仓库中没有产品, 则生产者将产品放入仓库, 否则停止生产并等待, 直到仓库中的产品被消费者取走为止

    • 如果仓库中放有产品, 则消费者可以将产品取走消费, 否则停止消费并等待, 直到仓库中再次放入产品为止

    • 在生产者消费者问题上, synchronized 可阻止并发更新同一个共享资源, 实现了同步,synchronized 不能用来实现不同线程之间消息传递 (通信)

  • Java的解决线程通信的方法

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

    注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 illegalMonitorStateException

解决方式

管程法

  • 生产者: 负责生产数据的模块 (可能是方法, 对象, 线程, 数组)
  • 消费者: 负责处理数据的模块 (可能是方法, 对象, 线程, 数组)
  • 缓冲区: 消费者不能直接使用生产者的数据, 他们之间有个缓冲区
//产品
public class Product {
    private int id;
    public Product(int id) {
        this.id = id;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}
//消费者
public class ConsumerThread extends Thread {
    private SyncContainer syncContainer;
    public ConsumerThread(SyncContainer syncContainer) {
        this.syncContainer = syncContainer;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Product pop = syncContainer.pop();
            System.out.println("消费了第"+ pop.getId() + "号产品");
        }
    }
}
//生产者
public class ProviderThread extends Thread {
    //创建好的缓冲区
    private SyncContainer syncContainer;
    public ProviderThread(SyncContainer syncContainer) {
        this.syncContainer = syncContainer;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了" + i + "只鸡!");
            syncContainer.push(new Product(i));
        }
    }
}
//容器
public class SyncContainer {
    //容器大小
    Product[] products = new Product[10];
    //容器计数器
    int count = 0;
    //生产者放入产品
    public synchronized void push(Product product){
        //如果容器满了,就要等待消费者
        //通知消费者消费,生产者等待
        if(count == products.length) {
            try {
                this.wait();
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //如果容器没有满,就要生产产品
        products[count] = product;
        count++;
        //可以通知消费者消费
        this.notifyAll();
    }
    //消费者消费产品
    public synchronized Product pop(){
        //判断容器是否为空
        if(count == 0){
            //通知生产者生产.消费者等待
            try {
                this.wait();
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //如果可以消费
        count--;
        Product product =  products[count];
        //通知生产者生产
        this.notifyAll();
        return product;
    }
}
public class Main {
    public static void main(String[] args) {
        SyncContainer container = new SyncContainer();
        new ProviderThread(container).start();
        new ConsumerThread(container).start();
    }
}

信号灯法

//产品-->节目
public class TV {
   //演员表演,观众等待
   //观众观看,演员等待
   String voice;   //表演的节目
   boolean flag = true;
   //表演
   public synchronized void play(String voice) {
       if(!flag){
           try {
               this.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       System.out.println("演员表演了" + voice);
       //通知观众观看
       this.voice = voice;
       this.notifyAll();
       this.flag = !flag;
   }
   //观看
   public synchronized void watch(){
       if(flag){
           try {
               this.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       System.out.println("观众观看了" + voice);
       //通知演员表演
       this.notifyAll();
       this.flag = !flag;
   }
}
public class Player extends Thread {
   private TV tv = null;
   public Player(TV tv) {
       this.tv = tv;
   }
   @Override
   public void run() {
       for (int i = 0; i < 100; i++) {
           tv.play("表演了" + i + "号节目");
       }
   }
}
public class Watcher extends Thread {
   private TV tv = null;
   public Watcher(TV tv) {
       this.tv = tv;
   }
   @Override
   public void run() {
       for (int i = 0; i < 100; i++) {
           tv.watch();
       }
   }
}
//测试生产者消费者问题:信号灯法,标志位解决
public class Main {
   public static void main(String[] args) {
       TV tv = new TV();
       new Watcher(tv).start();
       new Player(tv).start();
   }
}

线程池

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

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

  • 好处

    • 提高了响应速度 (减少了创建新线程的时间)
    • 降低资源消耗 (重复利用线程池中线程, 不需要每次都创建)
    • 便于线程管理
      • corePoolSize: 核心池的大小
      • maximumPoolSize: 最大线程数
      • keepAliveTime: 线程没有任务时最多保持多长时间后会终止
  • 使用

    • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutors

      • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
      • Future submit(Callable task):执行任务,有返回值,
        —般又来执行Callable
      • void shutdown():关闭连接池
    • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

      public class TestPool {
          public static void main(String[] args) {
              //1.创建服务,创建线程池
              ExecutorService service = Executors.newFixedThreadPool(10);
              service.execute(new MyThread());
              service.execute(new MyThread());
              service.execute(new MyThread());
              service.execute(new MyThread());
              service.execute(new MyThread());
              //2.关闭链接
              service.shutdown();
          }
      }
      class MyThread implements Runnable {
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  System.out.println(Thread.currentThread().getName() + i);
              }
          }
      }
      

参考链接

posted @ 2022-01-26 14:06  Faetbwac  阅读(85)  评论(0编辑  收藏  举报