java基础篇--线程

文章目录

    • 1、多线程的创建
      • 1.1 继承Thread类
      • 1.2 实现Runnable接口
      • 1.3 实现Callable、FutureTask接口
    • 2、线程死锁
      • 2.1 预防死锁
      • 2.2 避免死锁
    • 3、线程安全问题
      • 3.1 同步代码块
      • 3.2 同步方法
      • 3.3 Lock锁
      • 3.4 线程通信
    • 4、线程池

1、多线程的创建

线程与进程:

  • 进程是程序的一次执行过程,资源分配的基本单位,即内存分配资源时以进程为单位

  • 线程是一个程序内部的一条执行路径。

    • 可以理解为进程中的多个任务,线程即对应进程中的单个任务,同个进程的线程共享资源。

    • 线程是调度的基本单位,即可以进行进程内调度切换,相对于进程间调度切换,节省了一定时间。

同个进程内的线程切换:类似于你在家里做家务,所处的环境不需要改变,即资源相同;不同进程间的线程切换/以进程为单位的切换:需要先保存原有进程执行的情况,如程序计数器pc等(保证下次执行能够恢复原有环境),再为现进程营造一个所需环境。线程执行开销小,但不利于资源的管理和保护,而进程正好与线程相反。

上下文切换:

线程切换时需保存原有线程执行的情况也就是上下文,如程序计数器、栈信息等;保存线程的进度,下次该线程得到CPU调度时,能够继续恢复原执行的情形,得以继续执行下去。

发生上下文切换的时机:

  • 主动让出 CPU,比如调用了 sleep(), wait()
  • 时间片用完,操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。

java线程的6种状态

线程状态 描述
NEW新建 线程刚被创建,但未启动
Runnable可运行 线程已经调用了start()等待CPU调度处于ready就绪状态;得到CPU调度则处于running运行状态
Block阻塞 线程在执行的时候未竞争到锁对象,进入阻塞状态
Waiting无限等待 进入等待状态,需另一个线程唤醒
Timed Waiting计时等待 调用含有超时参数的方法(sleep)进入计时等待状态
Teminated被终止 正常运行结束或因没有捕获的异常终止了run方法而死亡

java基础篇--线程_第1张图片

wait()sleep()的区别

  1. wait方法 是Object类的静态方法,sleep方法是Thread类的静态方法
  2. wait通常用于进程交互/通信,wait()被调用后,线程不会自动苏醒,需要另外的线程调用同一个对象上的notify()、notifyAll()方法唤醒。wait(long timeout)超时后线程会自动苏醒;sleep()方法执行完毕后,会自动苏醒
  3. wait()释放锁,只在同步代码块/方法中使用;sleep()不释放锁
  4. 两个方法都可以暂停线程的执行

1.1 继承Thread类

具体步骤:

  1. 继承Thread类实现
  2. 重写run方法
  3. 创建new新线程对象
  4. 调用start方法启动线程(执行的还是run方法)

为什么不直接调用了run方法,而是调用start启动线程?

答:直接调用run方法会当成普通方法执行,此时相当于还是单线程执行;只有调用start方法才是启动一个新的线程执行,相当于告诉操作系统,这里有一个新的线程要分配CPU。

同时编程时不能把主线程任务放在子线程之前,这样主线程就一直是先跑完的,相当于一个单线程。

优点:编码简单;

缺点:线程类已经继承Thread类,无法再继承其他类,不利用扩展。

1.2 实现Runnable接口

具体步骤:

  1. 定义一个线程任务类实现Runnable接口,重写run()方法
  2. 创建对象
  3. 把对象交给Thread处理
  4. 调用线程对象的start()方法启动线程
//第一种写法
class MyRunnable implements Runnable{
	@Override
	public void run(){
		//线程运行内容
	}
	
}
public class Thread{
	public static void main(String[] args){
		Runnable target = new MyRunnable();//创建任务类对象
		new Thread(target).start();//启动线程
		
		//第二种写法 lambda表达式
		new Thread(()->{
			//线程运行内容
		})
	}
	
}

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的,不能抛出异常。

1.3 实现Callable、FutureTask接口

前两种创建方式都存在一个问题:重写的run方法均不能直接返回结果,不适合需要返回线程执行结果的业务场景。

  1. 得到任务对象

    定义类实现Callable接口,重写call方法,封装要做的事情;

    用FutureTask把Callable对象封装成线程任务对象

  2. 把线程任务对象交给Thread处理

  3. 调用Thread的start()方法启动线程,执行任务

  4. 线程执行完毕,通过FutureTask的get方法获取任务执行结果

//应该申请线程任务执行完毕后的结果数据类型
class MyCallable implements Callable<String>{
	@Override
	public void call() throws Exception{
		//线程运行内容
		return "线程执行结果";
	}
	
}
public class Thread{
	public static void main(String[] args){
		Callable<String> call = new MyCallable();//创建任务类对象
		//把任务对象交给FutureTask对象 继承了Runnable接口
		FutureTask<String> f = new FutureTask<>(call);
		
		new Thread(f1).start();//启动线程
	}
	
}

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行结果。

缺点:编码复杂。

2、线程死锁

死锁的概念:在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象。

//死锁例子,资源1和2都拿着对方想要的资源的同时,又互相等待对方手里的资源
class DeadLock {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                try {
                    Thread.sleep(1000);//若线程1先执行,锁住资源,让出CPU ,让线程2得以运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    //执行
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {//若线程2先执行,锁住资源,让出CPU ,让线程1得以运行
                }
            }
        }, "线程 2").start();
    }
}

死锁必须满足以下四个条件:

1、互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁;

2、不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放;

3、请求和保持条件:进程已经保持了至少一个资源,但又提出新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但对自己已有的资源保持不放。

4、循环等待条件:存在一种进程资源的循环等待链,链中的每个进程已获得的资源同时被下一个进程所请求。

循环等待是死锁的必要不充分条件。

2.1 预防死锁

破坏死锁所必备的条件:

破坏不剥夺条件:当某个进程请求新的资源得不到满足时,必须立即释放保持的所有资源,待以后需要时再重新申请。

破坏请求和保持条件:在运行前一次申请完所需要的全部资源。

破坏循环等待条件:采用顺序资源分配法,即破坏循环等待的现象。

2.2 避免死锁

银行家算法核心思想:在进程提出资源申请时,预先判断这次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。

银行家算法步骤:

  1. 检查此次申请是否超过了之前声明的最大需求数
  2. 检查此时系统剩余的可用资源是否还能满足此次请求
  3. 用安全性算法检查分配是否会导致系统进入不安全状态

安全性算法步骤:检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收,不断重复上述过程,看最终是否能让所有进程都加入安全序列。

判断依据:是否能找到安全序列,如果找不到安全序列,会使系统处于不安全状态,可能会发生死锁。

3、线程安全问题

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

发生的原因:

  1. 存在多线程并发
  2. 同时访问共享资源
  3. 存在修改共享资源

为了解决线程安全问题,线程同步

核心思想: 加锁,把共享资源加上锁,每次只能一个线程进入,访问完毕以后解锁,其他线程才能进来。

3.1 同步代码块

作用:把出线程安全问题的核心代码上锁

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

synchronized(同步锁对象){
	操作共享资源的代码
}

锁对象的规范要求:

规范上:建议用共享资源作为锁对象

  1. 对于实例方法建议使用this作为锁对象
  2. 对于静态方法建议使用类名.class作为锁对象

3.2 同步方法

作用:把出现线程安全问题的核心方法上锁

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

格式:

修饰符 synchronized 返回值类型 方法名称(形参列表){
	操作共享资源的代码
}

同步方法底层原理:

  1. 底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  2. 对于实例方法,默认用this作为锁对象,但代码要高度面向对象
  3. 对于静态方法,默认使用类名.class作为锁对象

同步代码块好还是同步方法好?

同步代码块锁的范围更小,同步方法锁的范围更大(实际开发还是同步方法使用更多,使用方便)。

3.3 Lock锁

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

获得Lock锁的实现类对象 -> ReentrantLock()

Lock的基本API :获得锁 lock();解锁unlock()

使用try-finally结构,在finally代码块进行解锁,保证代码块能够得到解锁。

3.4 线程通信

所谓线程通信就是线程间相互发送数据。

常见形式:通过共享一个数据的方式实现,根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。

三个常见方法:(使用当前同步锁对象进行调用

方法名称 说明
void wait() 当前线程等待,直到另一个线程调用notify()或notifyAll()唤醒自己
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

4、线程池

线程池是一个可以复用线程的技术。

不使用线程池的问题:

如果用户每发出一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销很大,这样会严重影响系统的性能。

线程池工作原理

线程池中的核心线程在处理请求,待完成的用户的请求位于任务队列,一旦处理的任务结束,就继续任务队列中的未完成的用户请求;核心线程都在处理请求且任务队列已满,会创建临时线程来帮忙处理。若临时线程数量达到规定上限、各线程皆忙且任务队列已满,则会根据设置的处理方式处理新发出的用户请求。

拒绝策略:

  • 直接拒绝
  • 拒绝并抛出异常(默认)
  • 移除最早未处理的请求
  • 让执行execute方法的程序执行,若该程序已关闭,则抛弃,该策略性能低

如何得到线程池对象

  • 方式一:使用Executor(线程池的工具类)调用方法返回不同特点的线程池对象

    1. FixedThreadPool :固定线程数量的线程池。该线程池中的线程数量始终不变。允许请求的队列长度为Integer.MAX_VALUE,堆积大量的请求,导致内存溢出OOM

    2. SingleThreadExecutor: 只有一个线程的线程池。允许请求的队列长度为Integer.MAX_VALUE,堆积大量的请求,导致内存溢出OOM。

    3. CachedThreadPool: 一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。允许创建的线程数量为Integer.MAX_VALUE,导致OOM

    4. ScheduledThreadPool:实现定时、周期性任务的线程。允许创建的线程数量为Integer.MAX_VALUE,导致OOM

    Executors工具类:实际调用ThreadPoolExecutor的构造方法。不适合做大型互联网场景的线程池方案,可能会造成资源耗尽的风险。

  • 方式二:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

java基础篇--线程_第2张图片

核心线程即是线程池固有的线程,可以理解为正式员工;最大线程池包含核心线程+临时线程,临时线程可以理解为临时员工,当人手不够时会招收临时员工;任务队列即是店外等待的客户的队列;线程工厂可以理解为HR,招收临时员工,即根据情况创建临时线程。

线程池常见面试题:

  • 临时线程什么时候创建

    新任务提交时,发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程时,此时才会创建临时线程。

  • 什么时候会开始拒绝任务

    核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝。

执行线程的 execute()方法和 submit()方法的区别

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

你可能感兴趣的:(java基础篇,多线程与高并发,java,线程池,线程安全,多线程)