7_多线程编程

多线程

线程简介

多任务:边吃饭边玩手机、开车打电话。(看起来是多个任务都在做,其实本质上大脑在同一时间依旧只做了一件事情)。

多线程:开黑。

进程:在操作系统中运行的程序就是进程。一个进程可以有多个线程,如视频中同时听声音、看图像、看弹幕。

一个进程可以包含若干个线程,至少有一个线程

线程创建

Thread、Runnable、Callable

Thread

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class TestThread01 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0;i<20;i++){
            System.out.println("******"+i);
        }
    }
    public static void main(String[] args) {
      //main线程,主线程

        //创建一个线程对象
        TestThread01 testThread01 = new TestThread01();
        //调用start()方法开启多线程
        testThread01.start();
        for (int i = 0; i<20; i++){
            System.out.println("########"+i);
        }
    }
}
//线程开启不一定立即执行,由CPU调度(可能没次错出现的顺序有区别)

Runnable

  • 定义MyRunnable类实现Runnable接口
  • 实现Run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
package com.TestThread;

//创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法。
public class TestThread03 implements Runnable{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0;i<20;i++){
            System.out.println("******"+i);
        }
    }
    public static void main(String[] args) {
        //main线程,主线程
        //创建Runnable接口的实现类对象
        TestThread03 testThread03 = new TestThread03();
        //创建线程对象,通过线程对象来开启我们的线程,代理;
//        Thread thread = new Thread(testThread03);
//        thread.start();
        new Thread(testThread03).start();
        for (int i = 0; i<20; i++){
            System.out.println("########"+i);
        }
    }
}

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

实现Callable接口

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

Calllable的好处:

  1. 可以定义返回值
  2. 可以抛出异常

静态代理

  1. 真实对象和代理对象都要实现同一个接口

  2. 代理对象要代理真实角色

​ 好处:

  1. 代理对象可以做真实对象做不了的事情
  2. 真实对象专注做自己的事情

Lamda表达式

(params)->expression[表达式]

(params)->statement[语句]

(params)->{statements}

函数式接口:只有一个方法的接口

lamda表达式:前提是接口为函数时接口

线程

线程状态

  1. 创建状态
  2. 就绪状态
  3. 阻塞状态
  4. 运行状态
  5. 死亡状态

线程方法

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

停止线程

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

线程休眠

  • 每一个对象都有一个锁,sleep不会释放锁

线程礼让

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

Join

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

线程状态观测

Thread.State

新生->就绪->运行->死亡/阻塞

  • NEW:尚未启动的线程处于此状态。新生
  • RUNABLE:再Java虚拟机中执行的线程处于此状态。运行
  • BLOCKED:被阻塞等待监视器锁定的线程处于此状态。阻塞
  • WAITING:正在等待另一个线程执行动作达到特定动作的线程处于此状态。阻塞
  • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。阻塞
  • TERMINATED:已退出的线程处于此状态。死亡

一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

线程优先级

  • 线程的优先级用数字表示,范围从1~10。
  • 改变获获取优先级:getPriority().setPriority(int xxx)
  • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度

守护(daemon)线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如后台记录操作日志、监控内存、垃圾回收等

线程同步

多个线程操作同一个资源

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

同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
public synchronized void method(int args){}
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
  • 缺陷:若将一个大的方法声明为synchronized将会影响效率

同步块

synchronized (Obj){}

  • Obj称为同步监视器
    • Obj可以是任何对象,但推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class。
  • 同步监视器的执行过程
    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没锁,然后锁定同步监视器并访问

死锁

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

  • 死锁避免方法

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

Lock

  • 通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。

synchronized与Lock的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock > 同步代码(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

线程通信

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

线程池

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

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

  • 好处:

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

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

posted @ 2023-02-26 17:47  鹅四砸砸灰  阅读(53)  评论(0)    收藏  举报