Java中的多线程和多进程

2022-10-25 15:36:28 浏览数 (3)

Java中的多进程和多线程

  • 一、线程和进程的概念
  • 二、Java中创建线程
  • 三、线程状态
  • 四、进程的分类
  • 五、线程同步
  • 六、死锁
  • 七、面试中的问题

一、线程和进程的概念

项目开发目标:高可用、高性能、高并发

区别

进程

线程

根本区别

作为资源分配的单位

调度和执行的单位

开销

每个进程都有独立的代码和数据空间(进程上下文),进程间的切换开销大

同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器PC,线程的切换开销小(部分寄存器)

所处环境

OS中能同时运行多个任务/程序

同一个应用程序中有多个顺序流同时执行

分配内存

系统在运行时会为每个进程分配不同的内存区域

除了CPU外,不会为线程分配内存,只能共享那个所在线程的资源,拥有相同的地址空间

包含关系

没有线程的进程可以被视为单线程的,如果一个进程拥有多个线程,则执行过程不是一条直线的,而是多条线共同完成

线程是进程的一部分,所以线程被称为轻权或轻量级进程

注意:大多线程是模拟出来的(感官上的多线程同步),真正的多线程指的是有多个CPU/核。

核心概念:

  • 线程就是独立的执行路径
  • main()称之为主线程,为系统的入口点,用于执行整个程序;
  • 一个进程中开辟了多个线程,线程的运行由调度器安排调度,调度器是与OS紧密相关的,先后顺序无法人为干预;
  • 对于同一份资源操作时会存在抢夺问题,需要加入并发控制;
  • 线程会带来额外的开销,如CPU调度时间、并发控制开销;
  • 每个线程在自己的工作内存(与主内存交互)交互,加载和存储主内存控制不当会造成数据不一致。

二、Java中创建线程

继承Thread类(implements Runnable) 实现Runnable接口(abstract run()) 实现Callable接口(JUC并发包) 注意:Java中少用继承 extends 多用实现 implements,避免单继承的局限性。一个类仅在他们需要被加强或者修改时才会被继承。 start()方法能够异步地调用普通方法run(),才能达到多线程的目的。

代码语言:javascript复制
//方法一:继承Thread
class PrimeThread extends Thread{
    long value;

    public PrimeThread(long value) {
        this.value = value;
    }

    @Override
    public void run() {
        System.out.println("通过继承Thread创建了进程");
    }
}

//方法二:实现Runnable
class PrimeRun implements Runnable{
    long value;

    public PrimeRun(long value) {
        this.value = value;
    }

    @Override
    public void run() {
        System.out.println("通过实现Runnable创建了进程");
    }
}

public class TestThread {

    public static void main(String[] args) {
        PrimeThread primeThread = new PrimeThread(123);
        primeThread.start();//创建一个进程,Thread类里有run方法,交给CPU并不保证运行

        PrimeRun primeRun = new PrimeRun(123);
        Thread thread = new Thread(primeRun);//借助Tread代理对象
        thread.start();

        //如果只使用一次,可以使用匿名创建
        new Thread(new PrimeThread(123)).start();
        //为了区分,可以在Thread中添加名称
        new Thread(new PrimeRun(123), "new Thread").start();
        //输出进程名,放在这里是main进程
        System.out.println(Thread.currentThread().getName());
    }
}

方法三:
继承Callable,加上泛型,重写call()方法,可能抛出异常。
调用时:用到了服务和线程池
1. 创建目标对象:Class  class = new Class();
2. 创建执行服务:ExecutorService see = Executors.newFixedThreadPool(1);//固定大小的线程池,可控制最大并发量,还有Cached可缓存、Single单线程化FIFO、Scheduled大小无限周期性的ThreadPool
3. 提交执行:Future< Boolean> result1 = ser.submit(class);
4. 获取结果:boolean r1 = result1.get();
5. 关闭服务:ser.shutdownNow();

当一个资源有多个代理处理时,可能存在网络延时,存在并发问题,需要保存线程安全。

为什么要用线程池: 可重用

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

三、线程状态

新生状态:线程对象创建 就绪状态:调用start()方法;阻塞解除;运行时调用yield()方法(没有其他等待线程,当前线程立即恢复执行);JVM切换进程 运行状态:线程真正执行线程体的代码块 阻塞状态:调用sleep()仍占用资源、wait()释放现有资源、join()函数等到另一个线程执行完再继续执行;I/O进行read()/write() 死亡状态:线程体的代码执行完毕或终端执行,一旦进入死亡状态,不能再调用start()再次启动

setDaemon(Boolean b):将指定的线程(线程启动前)设置成后台线程/守护线程,创建用户线程的线程结束时,守护线程也随之消亡; setPriority(int newPriority)、getPriority():线程优先级代表的是概率,范围从1到10,默认为5; stop():停止线程,不推荐使用。

四、进程的分类

线程分为用户线程和守护线程(Daemon)。 虚拟机必须保证用户线程执行完毕,而不用等待守护线程(如后台记录操作日志、监控内存等)执行完毕。线程默认都是用户进程。

代码语言:javascript复制
Thread t = new Thread();
t.setDaemon(true);//设置为守护进程
t.start();//必须在线程执行前设置

五、线程同步

对于同一个资源,多个线程同时访问。 解决办法:队列(queue) 锁(synchronized),还有一种Lock方法

锁操作的关键:目标对、效率高 锁机制存在的问题: 一个线程持有锁会导致其他需要此资源的线程挂起; 再多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题; 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

思路:通过private关键字来保证数据对象只能被方法访问,然后利用synchronized机制(synchronized方法和synchronized块)。 缺陷:若将一个大的方法声明为synchronized将会大大影响效率。

代码语言:javascript复制
//synchronized方法,锁的是对象的资源
public synchronized void test(){
	...
}

//synchronized块,obj是共享资源的对象
synchronized(obj){
	...
}
//同步方法中不需要制定同步监视器obj,因为同步方法的监视器就是this,即该对象本身或Class。

普通块/局部块、构造块、静态块、同步块 同步块目标更明确,同步方法锁的是this。提高性能:在同步块之前添加一些特殊情况的判断,避免全都等待。

六、死锁

过多的同步可能造成互相不释放资源,从而相互等待。一般发生在同步中持有多个对象的锁。 避免:不要在同一个锁块中嵌套锁。

避免死锁的算法:银行家算法

七、面试中的问题

1. 程序、进程和线程

  • 程序 是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程可以请求资源和调度,是一个动态的概念。
  • 进程 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 程序是指令、数据及其组织形式的描述,进程是程序的实体,一个程序可能有多个进程。
  • 线程 有时被称为轻量级进程,是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立、可调度的执行单元,是系统独立调度和分派CPU的基本单位,也指运行中的程序的调度单位。

整个outlook应用程序代码是一个程序;打开一个outlook是一个进程,打开一个word是另一个进程;而发邮件是outlook进程的一个线程,收邮件又是另一个线程。

2. 多线程和多进程

  • 多进程 特点:内存隔离,单个进程的异常不会导致整个应用的崩溃,方便调试;但是进程见调用、通信和切换的开销大。 常使用在目标子功能间交互少的场景,弱相关性的、可扩展到多机分布(Nginx负载均衡)的场景。
  • 多线程 特定:提高了系统的并行性,开销小;但是没有内存隔离,单个线程崩溃会导致整个应用的退出,定位不方便。 常使用在存在大量I/O,网络等耗时操作,或者需要与用户进行交互,频繁创建和销毁的Web服务、大量计算的、强相关性的、多核分布(多核CPU)的场景。

性能:多进程的程序要比多线程的程序健壮。 注意:Linux中以“未分配资源的进程描述线程”: 实际上,从内核的角度来看,Linux并没有线程的概念;是否共享地址空间几乎是进程与线程之间的本质的唯一区别。

3. 并行和并发

  • 并行 指在同一时刻,有多条指令在多个处理器上同时执行;
  • 并发 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上(时间段内)具有多个进程同时执行的效果,但在微观上(时间点上)并不是同时执行的,只是把时间分成若干端,使多个进程快速交替的执行。

多线程并行还是并发,取决于分配到的CPU资源,如果只有一个需要线程抢夺就是并发;如果有多个线程分配到就是并行。

4. 多线程同步方式 (1)synchronized关键字(JVM托管) 方法和块。 (2)wait方法和notify方法 wait方法释放对象锁,进入等待状态,调用notify方法通知正在等待的线程。 (3)Lock(代码实现) lock()以阻塞方式获得锁,且阻塞态会忽略interrupt方法; tryLock()以非阻塞方式获得锁,可以加时间参数; lockInterruptibly()不同于lock()不会忽略interrupt方法。

5. 进程停止运行(停止:4,5)方法 (1)sleep() Thread类的静态方法,线程控制自身流程,不释放锁不通信,位置任意,需要捕获异常interrypt。 (2)wait() Object类的方法,用于线程间通信,释放锁,放在同步块中,无异常。 (3)yield() 给相同优先级或更高优先级的线程让出锁,自己进入可执行状态,无异常。 (4)stop() 释放已经锁定的所有监视资源。 (5)suspend() 挂起不会释放锁,会发生死锁。 (6)join() 参数为时间,等待后执行(在执行完run之后)。

0 人点赞