关于多线程

目录

cpu

进程/线程

多线程优点

并行/串行

CPU时间片

计算密集型/IO密集型

上下文切换

创建方式

继承Thread类创建线程

实现Runnable接口创建线程

使用Callable和Future创建线程

使用线程池例如用Executor框架

使用匿名内部类的形式创建线程

使用lambda表达式创建线程

线程池

线程池概念

ThreadPoolExecutor

Java线程池的工作流程

拒绝策略

线程池参数设置

线程状态

新建

就绪

运行

阻塞

死亡

线程的基本方法

线程等待:wait方法

线程睡眠:sleep方法

线程让步: yield 方法

线程中断:interrupt方法

线程加入 join方法

线程唤醒: notify方法

leep方法与 wait方法的区别如下

start方法和run方法的区别


cpu

​                 CPU的中文名称是中央处理器,是进行逻辑运算用的,主要由运算器、控制器、寄存器三部分组成,从字面意思看就是运算就是起着运算的作用,控制器就是负责发出cpu每条指令所需要的信息,寄存器就是保存运算或者指令的一些临时文件,这样可以保证更高的速度。也就是我们的线程运行在cpu之上。

进程/线程

           进程是资源分配最小单位,线程是程序执行的最小单位。 计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程

注:进程是资源分配最小单位,线程是程序执行的最小单位

  • 什么是进程:

    1. cpu从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程

    2. 一个程序如果被cpu多次被读取到内存中,则变成多个独立的进程

  • 什么是线程:

    线程是程序执行的最小单位,在一个进程中可以有多个不同的线程同时执行。

多线程优点

                    采用多线程的形式执行代码,目的就是为了提高程序的效率。    

并行/串行

        串行也就是单线程执行 代码执行效率非常低,

        代码从上向下执行; (同步操作)

        并行就是多个线程并行一起执行,效率比较高。异步操作)

        并发指两个或多个事件在同一个时间段内发生。

                

CPU时间片

  1. 单核的cpu上每次只能够执行一次线程,如果在单核的cpu上开启了多线程,则会发生对每个线程轮流执行 。

  2. Cpu每次单个计算的时间成为一个cpu时间片,实际只有几十毫秒人为感觉好像是在多线程。

  3. 对于线程来说,存在等待cpu调度的时候 该线程的状态是为就绪状态,如果被cpu调度则该线程的状态为运行状态

  4. 当cpu转让执行其他的线程时,则该线程有变为就绪状态。

计算密集型/IO密集型

                计算密集型:长时间占用cpu;例如: 视频剪辑

​                IO密集型 :cpu计算时间短 , 访问外接设备时间长Input/output

上下文切换

                多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候,就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

创建方式

- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
- 使用线程池例如用Executor框架
- 使用匿名内部类的形式创建线程
- 使用lambda表达式创建线程

              

继承Thread类创建线程

                

public class ThreadDemo01 extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"在运行");
    }

    public static void main(String[] args) {
        ThreadDemo01 demo01 = new ThreadDemo01();
        demo01.start();
    }
}

实现Runnable接口创建线程

               

public class ThreadDemo02 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ",我是子线程");
    }

    public static void main(String[] args) {
        new Thread(new ThreadDemo02()).start();
    }
}

使用Callable和Future创建线程

                从Java 5开始,Java提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法,可以看作是线程的执行体,但call()方法比run()方法更强大。

        call()方法可以有返回值。

        call()方法可以声明抛出异常。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return 1;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadCallable callable = new ThreadCallable();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        new Thread(futureTask).start();
        Integer result = futureTask.get();
        System.out.println(result);
    }
}

使用线程池例如用Executor框架

                

public class ThreadDemo03{
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        //方法参数是Runnable的实现类
        service.execute(()->System.out.println(Thread.currentThread().getName() + ">我是子线程<"));
    }
}

使用匿名内部类的形式创建线程

                

public class ThreadDemo04 {
    
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}

使用lambda表达式创建线程

public class ThreadDemo04 {

    public static void main(String[] args) {
        new Thread(()->System.out.println(Thread.currentThread().getName())).start();
    }
}

         Thread和Runnable的区别

        ​ 实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

线程池

线程池概念

​ 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

  如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。

​ 线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行

ThreadPoolExecutor

 public ThreadPoolExecutor(int corePoolSize, //核心线程    
                           int maximumPoolSize,//最大线程数
                           long keepAliveTime,//存活时间
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue) {//队列
     
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          Executors.defaultThreadFactory(), defaultHandler);
 }

          

  • corePoolSize:线程池中核心线程的数量

  • maximumPoolSize:线程池中最大线程的数量

  • keepAliveTime:当前线程数量超过corePoolSize时,空闲线程的存活时间

  • unit:keepAliveTime的时间单位

  • workQueue:任务队列。被提交但尚未被执行的任务存放的地方

      

Java线程池的工作流程

Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。

  • 如果正在运行的线程数量少于 corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。

  • 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。

  • 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。

  • 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。

  • 在线程任务执行完毕后、该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。

  • 在线程处于空闲状态的时间超过 keepAliveTime 时间时,正在运行的线程数量超过corcPoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corcPoolSize大小。

拒绝策略

        

                若线程池中的核心线程数被用完,且阻寒队列已排满,线程池中最大线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了促证操作系统的安,,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝箸略有AbortPolicy .CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy 这4 种,默认的拒绝策略在ThreadPoolExecutor 中作为内部类提供。在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。         

  • AbortPolicy :直接抛出异常,阻止线程正常运行

  • CallerRunsPolicy:如果被拒绝的任务线程未关闭,则执行这个任务

  • DiscardOldestPolicy:移除线程队列中最早的线程任务

  • DiscardPolicy : 丢弃当前的线程任务不做任何处理。

线程池参数设置

                计算密集型(CPU密集型) 的任务比较消耗cpu,所以一般核心线程数设置的大小等于或者略微大于 cpu的核数;一般为N+1(N为CPU的核数)

                磁盘密集型(IO密集型) 的任务主要时间消耗在 IO等待上,cpu压力并不大,所以线程数一般设置较大。IO密集型一般为2*N(N为CPU的核数)

线程状态

新建

                当用new关键字创建一个线程时,还没调用start 就是新建状态。

就绪

                调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。

运行

                当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。

阻塞

       当遇到以下几种情况,线程会从运行状态进入到阻塞状态。

  • 调用wait方法,jvm会将线程放入等待队列(waiting queue),使线程进入等待。

  • 当线程去获取同步锁的时候,锁正在被其他线程持有,jvm会将该线程放入锁池(lock pool)

  • 其他阻塞,运行状态的线程调用sleep方法、join方法,或者进入io请求的时候,会导致线程阻塞。

需要注意的是,阻塞状态只能进入就绪状态,不能直接进入运行状态。因为,从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定。只有当线程获得了CPU时间片之后,才会进入运行状态。

死亡

                当run方法正常执行结束时,或者由于某种原因抛出异常都会使线程进入死亡状态。另外,直接调用stop方法也会停止线程。但是,此方法已经被弃用,不推荐使用。

线程的基本方法

线程等待:wait方法

  • 调用wait()方法,线程会进入WATING状态,只有等到其他线程的通知活被中断后才会返回。需要注意的是,在调用wait()方法的后会释放对象的锁,因此wait()方法一般被用到同步方法或者同步代码块中。

线程睡眠:sleep方法

  • 用sleep方法会导致当前线程休眠。与 wait方法不同的是,sleep方法不会释放当前占有的锁,会导致线程进人 TIMED-WATING 状态,而 wait方法会导致当前线程进入WATING.

线程让步: yield 方法

调用yield 方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU 时间片。在一般情况下,优先级高的线程更有可能竞争到CPU时间片,但这不是绝对的,有的操作系统对线程的优先级并不敏感。

线程中断:interrupt方法

  • interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态(阻塞、终止等)。状态的具体变化需要等待接收到中断标识的程序的最终处理结果来判定。

线程加入 join方法

  • join方法用于等待其他线程终止,如果在当前线程中调用一个线程的 join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU 的使用权。在很多情况下,主线程生成并启动了子线程,需要等到子线程返回结果并收集和处理再退出,这时就要用到join方法,具体的使用方法如下:

System.out.println(”子线程运行开始!"):
ChildThread childThread - new ChildThread();
childThread.join(0);//等待子线程childThread执行结束
System.out.println("子线join ()结束,开始运行主线程");

线程唤醒: notify方法

Object类有个 notify方法,用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的我们通常调用其中一个对象的 wait方法在对象的监视器上等待直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他线程竞争。类似的方法还有notifyAll,用于唤醒在监视器上等待的所有线程。

leep方法与 wait方法的区别如下

        

  • sleep方法属于Thread类,wait方法则属于Object类。

  • sleep方法暂停执行指定的时间,让出 CPU 给其他线程,但监控器状态依然保持在指定的时间过后又会自动恢复运行状态。

  • 在调用sleep方法的时候线程不会释放对象锁。

  • 在调用wait方法时,线程会放弃对象锁,进入等此对象的等待锁池,只有针对此对象调用notify方法后,该线程才能进人对象锁池准备获取对象锁,并进入运行状态。

start方法和run方法的区别

  • start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕,就可以继续执行下面的代码。

  • 在通过调用 Thread 类的 start方法启动一个线程时,此线程处于就绪状态,并没有运行。

  • run方法也叫作线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态,开始运行 run方法中的代码。在run方法运行结束后,该线程终止,CPU再调度其他线程

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值