多线程

一.程序、进程和线程这三种知识的基本概念

1、程序(program)

为了完成特定任务用某种语言编写的一组指令集合。级指一段静态代码,静态对象。

2、进程(process)

  1. 进程是程序的一次执行过程或是正在运行的一个程序,进程也是正在运行的程序的实例。
  2. 进程是一个动态的过程,它有着自身的产生、存在和消亡的过程(也就是生命周期)。
    例如后台运行的QQ、游戏、输入法与杀毒软件等等各种软件。
  3. 进程作为资源分配的单位,系统在运行的时候会给每个进程分配不同的内存区域。

注意:程序是静态的,进程是动态的

3、线程(thread)

  1. 进程可以进一步细化为线程,是一个程序内部的一条执行路径。
  2. 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,而且线程切换的开销较小。
  3. 一个进程中的多个线程共享相同的内存单元(内存地址空间),它们在同一个堆中分配对象,可以访问相同的变量和对象。这使得线程之间的通信更加简便和高效,但是多个线程操作共享的系统资源可能就会带来安全隐患

4、总结

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

二.多线程

1.为什么要使用多线程

以单核CPU为为例,只使用单个线程先后完成多个任务(或调用多个方法),此时比用多个线程来完成用的时间更短,因为完成每个任务的时间是不变的,而在CPU为单核时候切换线程需要花费更多的时间,而且对线程进行管理要求额外的 CPU开销,那我们为什么要使用多线程呢?

原因:

  1. 多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态。
    我们电脑和手机常常打开运行多个软件,我们在多个运行中的软件切换时不会出现某个软件需要重新打开的现象
  2. 占用大量处理时间的任务使用多线程可以提高CPU利用率。
    一个任务有时候用不了所有的CPU资源,这时候可以把多余的CPU资源用于其他任务提高CPU利用率
  3. 改善程序结构。将耗时长且复杂的进程分为多个线程独立运行,这边便于理解和修改
  4. 多线程可以分别设置优先级以优化性能

多核CPU才能更好的发挥多线程的效率
举个例子

  1. 高速公路的入口处有多个收费通道(多个线程),每个收费通道可能配有0或1名收费人员(线程执行
  2. 如果是单核CPU的话,只有一名收费人员,收费人员在每个收费通道中跑来跑去给过往车辆收费,但是收费人员跑的速度非常快(CPU切换线程速度非常快)所以有多名收费人员同时工作的假象,大家觉得好像觉得所有通道都能通车(总的时间=每辆车通过收费站的时间+收费人员跑动的时间)。
  3. 如果是多核CPU的话,有多名收费人员(几核就几个收费人员),收费人员在每个收费通道中跑来跑去给过往车辆收费,效率更高(总的时间=每辆车通过收费站的时间+收费人员跑动的时间+管理收费人员跑动更换的时间
  4. 有多名收费人员,所以收费人员跑动的时间(CPU切换线程)大大减少,管理收费人员跑动更换的时间(后台线程管理)更是微不足道,所以多核CPU才能更好的发挥多线程的效率。(现在的服务器基本都是多核的)

2.是么时候才需要使用多线程

  1. 程序需要同时执行两个或多个任务。
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 需要一些后台运行的程序时。
    注:一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

什么时候使用最好?

  1. 耗时或大量占用处理器的任务阻塞用户界面操作;
  2. 各个任务必须等待外部资源 (如远程文件或 Internet连接)
    例如我们在使用淘宝购买商品的时候,手机会加载出图片和商品的介绍文字,但是图片的资源消耗比较大加载慢,所以为了不妨碍体验效果,我们一般都是图片加载和文字加载分开进行,这也是为什么在手机卡顿和网络卡顿的时候只加载出文字而图片空白在一段时间后才显示的原因。如果没有使用多线程的话那在加载完一个商品的图片和文字后才会加载下一个商品,如果配置低的话将会产生卡顿效果,给用户体验感较差

三.多线程创建

1、继承Thread类

特性

  1. 该方法的每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
  2. 启动线程是通过Thread对象的start()方法来启动这个线程,而非直接调用run()。

Thread的子类创建:

package com.pning;

/**
 * @Author Pning
 * @Date 2021/1/31 22:46
 **/

/**
 *  步骤:
 *      1.继承Thread
 *      2.重写run(),run()里面我们默认称线程体,里面存放要进行多线程的代码
 *      3.创建Thread子类的子对象,也就是是这个ThreadStudy
 */
public class ThreadStudy extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++) {
            if(i%2==0) {
                System.out.println(Thread.currentThread().getName()+ "  " + i);
            }
        }
    }
}

测试类

package com.pning;
/**
 * @Author Pning
 * @Date 2021/1/31 22:50
 **/
public class Test {
    public static void main(String[] args) {
        //创建Thead类的子类对象
        ThreadStudy threadStudy_1 = new ThreadStudy();
        ThreadStudy threadStudy_2 = new ThreadStudy();
        //通过创建的对象start()来自动启动当前线程(即调用当前线程的run()方法)
        threadStudy_1.start();
        threadStudy_2.start();
        for(int i=0;i<100;i++) {
            if(i%2!=0) {
                System.out.println("主线程==================="+i);
            }
        }
    }
}

运行结果(部分):
很明显可以看出有三个进程在交互执行:

  1. 线程1
  2. 线程2
  3. 主线程
    在这里插入图片描述

注意

  1. 我们不可以直接通过直接调用run()方法来启动线程,如果通过在Main方法里面run()方法的话,那此时的run()方法的线程是Main线程也就是主线程,这个程序也就不是多线程了。
  2. 已经start()过的线程对象不能再次start()

2、实现Runnable接口

Runnable接口的实现:

package com.pning;

/**
 * @Author Pning
 * @Date 2021/2/1 18:09
 **/
/**
 *  步骤:
 *      1.实现Runnable接口
 *      2.实现run()
 *      3.把实现接口的对象交给Thread的构造器中,创建Thread对象
 *      4.通过Thread的对象调用start()方法
 */
public class RunnableStudy implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<100;i++) {
            if(i%2==0) {
                System.out.println(Thread.currentThread().getName()+" "+ i);
            }
        }
    }
}

测试类

package com.pning;

/**
 * @Author Pning
 * @Date 2021/2/1 18:10
 **/
public class Test {
   public static void main(String[] args) {
    //创建实现Runnable的对象
       RunnableStudy runnableStudy = new RunnableStudy();
       Thread thread_1 = new Thread(runnableStudy);
       Thread thread_2 = new Thread(runnableStudy);
       thread_1.start();
       thread_2.start();
       for(int i=0;i<100;i++) {
           if(i%2!=0) {
               System.out.println("主线程==================="+i);
           }
       }
   }
}

运行结果(部分):
很明显可以看出有三个进程在交互执行:

  1. 线程1
  2. 线程2
  3. 主线程
    在这里插入图片描述
    为什么运行Thread的对象却会运行实现Runnable接口类的run()方法呢?
//这是Thread源码的一部分,有趣的是,其实Thread实际也是一个实现了Runnable接口的类
           //通过构造器的多态,将实现Runnable接口的对象交给线程初始化时候赋值
            public Thread(Runnable target) {
                init(null, target, "Thread-" + nextThreadNum(), 0);
            }
            //初始化赋值
             private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
               //。。。。。省略部分代碼。。。。。。。
                    this.target = target;
               //。。。。。省略部分代碼。。。。。。。
            }
            //线程启动时候,进行判断,如果有初始化有对target赋值的话则进行target的run()方法
             @Override
             public void run() {
                if (target != null) {
                    target.run();
                }
            }

3、实现Callable接口

4、线程池

5、小结

比起继承Thread来实现多线程,个人更喜欢实现接口的方式来实现多线程,有2个原因:

  1. 继承的话可能原本的类就需要一个父类,而java只支持单继承,如果原本的类就有一个父类的就使的逻辑更加混乱。
  2. 实现接口有一种天然的数据资源共享,不需要像继承Thread中需要在原本的属性上设置static修饰属性实现资源共享。

四.线程的调度

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,可以理解为各个线程轮流获得CPU的使用权,分别执行各自的任务。

1、调度的策略

  1. 时间片
    线程执行一段时间后切换另一个线程执行
  2. 抢占式
    给线程设置优先级,高优先级的线程优先抢夺CPU的资源

2、Java的调度方法

  1. 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
  2. 高优先级,使用优先调度的抢占式策略

3、线程的优先级

  1. 线程的优先等级
    有10个优先级,Thread里面已设定了三个常量
    注意的是,高优先级会优先抢占低优先级的CPU资源,但仅仅只是优先(可以理解为高概率获取CPU资源)而已,并不以意味着高优先级的线程比低优先级的先完成。
优先级数字对应常量
最大10Thread.MAX_PRIORITY
默认5Thread.NORM_PRIORITY
最小1Thread.MIN_PRIORITY
  1. 设置的方法
getPriority();//获取当前线程优先级
setPriority(Thread.MIN_PRIORITY);//获取线程优先级
  1. 说明
    (1)线程创建时继承父线程的优先级。
    (2)低优先级只是获取CPU资源调度的概率低,并不是一定要在高优先级线程执行完后再调用。

五.常用的方法

package com.pning;

/**
 * @Author Pning
 * @Date 2021/1/31 22:50
 **/
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.currentThread();//这是一个静态方法,获取当前线程对象

        Thread thread = new Thread();
        thread.setName("线程名");//设置当前线程的名称,要在start()方法被调用前
        thread.getName();//获取当前线程的名称
        thread.run();//通过我们需要重写的run()方法,里面是创建线程要执行的操作
        thread.start();//启动当前线程,调用当前线程的run()方法
        thread.join();//之前线程进入阻塞状态,join线程优先执行,等join线程执行完后才执行其他线程 ,但是会有InterruptedException
        thread.stop();//强制结束当前线程(非堵塞,是消亡),但是现在已经过时了,而且也不建议也使用
        thread.sleep(1000);//这是一个静态方法,可以直接调用也可以线程对象调用,在指定时间内线程进入堵塞转态,但是会有InterruptedException
        Thread.yield();//释放当前CPU资源,将线程从运行状态转到可运行状态,但有可能没有效果,因为有可能下一刻又被该线程抢到CPU资源
        thread.isAlive();//判断线程是否存活
        //下面是一种匿名线程的写法
        new Thread(){
            @Override
            public void run() {
                super.run();
            }
        };
    }
}

六.生命周期

1、线程的生命周期

不论是哪种方法实现的多线程,都必须在主线程中创建新的线程对象。一个完整线程的生命周期一般有以下五种状态。

  1. 新建
    当一个Thread类或者其子类兑现被声明并创建时,新生的线程对象就处于新建转态。
  2. 就绪
    处于新建转态的线程被start(),建进入线程队列转态等待CPU时间片,此时它只缺CPU资源就可以运行了。
  3. 运行
    处于新建转态的线程被的调度并获得CPU资源时,便进入运行转态,run()方法定义了线程的操作和功能。
  4. 阻塞
    某种特殊转态情况下,被人为的挂起或进行输入输出操作的时候,让出CPU并临时中止自己的执行,进入阻塞状态。
  5. 死亡
    线程完成了它的全部工作或者线程被提前强制性中止或出现异常导致结束。

2、Thread.Stata类表明java定义了线程的6种状态

Java语言使用Thread及其子类的对象来表示线程,下面是Thread内部的枚举类Stata的源码(删除了注释),可以发现java实际是将其区分为6个转态。

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

3、生命周期图

这个图片来自于尚硅谷视频的截图
在这里插入图片描述

七.线程安全问题

1、安全问题的提出

  1. 多线程执行的不确定性引起执行结果的不稳定
  2. 多线程对数据的共享,有可能应为操作的不完整(未完成就被抢走CPU资源)就会破坏数据

2、理解案例

仓库里面有a裤子200条,有三个请求

  1. 买60条
  2. 买150条
  3. 买190条
    在这里插入图片描述
    这是三个不同线程发送的请求,如果都执行到if里面的话,也就是说他们购买前查询到的库存都是200条,都满足购买条件,但是购买完后库存的剩余量变成了负数,这种情况中是非常严重的。

3、解决方法

知识点

1synchronized是非公平锁,不保证公平性,也就是一个对象在使用完线程后可再次抢到cpu资源而不是换下一个对象执行线程。
2同步监视器:可以是任何一个类对象,但是这个对象必须要有唯一性。讲人话就是这个对象在这个线程类只能new一次。

<一>synchronized方式
  1. 同步代码块
    这种方案用的是一个共用的锁,多个线程共用一把锁,否者还是会出现问题
synchronized (同步监视器){
//需要同步的代码
}

例如:不能写在run()里面创建的Object(这样每一次线程执行都会新建一次该对象),也不能直接直接用匿名对象(直接写new Object()这种写法也不行)。
案例理解
在卫生间中,门把手上有两种颜色显示(红/绿),红色代表有人在里面,绿色代表里面没人。同步代码块就是使用了这种方式,多个线程在“门口”排队,等到门里面里面的线程出来了在进去下一个。

  1. 同步方法
    用synchronized 修饰的方法的话,在这个方法里面可以看成是一个单线程,只能有一个线程参与,其他线程等待,效率较低。
public synchronized 返回类型方法名(){
   //需要同步的代码
}

注意
这种方式其实也是利用到了同步监视器
(1)synchronized作用于类的非静态方法上,同步监视器是调用这个方法的对象俗称“对象锁”
(2)synchronized作用于类的静态方法上,同步监视器是这个类俗称“类锁”

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

八.死锁问题

1、什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2、死锁产生的四个必要条件
  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程和资源的环形链。
3、如何尽量避免死锁
  1. 专门的算法和原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步
4、如何解决死锁
  1. 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  3. 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  4. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值