由于blog各种垃圾评论太多,而且本人审核评论周期较长,所以懒得管理评论了,就把评论功能关闭,有问题可以直接qq骚扰我

JAVA-多线程

JAVA 西门飞冰 173℃

多线程的一些概念

什么是并发:

并发就是指程序同时处理多个任务的能力。

并发编程的根源在于对多任务情况下对访问资源的有效控制。

程序、进程与线程

程序:是静态的概念,比如qq、微信、网易云音乐都是程序的一种。

进程:是动态的概念,是程序在运行的状态,进程说明程序在内存中的边界。

线程:线程是进程内的一个“基本任务”,每个线程都有自己的功能,是CPU分配与调度的基本单位。

并发与并行image-20220628111447290

并发:并发是指两个或多个事件在同一时间间隔发生。

并行:并行是指两个或者多个事件在同一时刻发生。

同步和异步

同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。

异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。

临界区

临界区用来表示一种公共资源与共享数据,可以被多个线程使用。 同一时间只能有一个线程访问临界区(阻塞状态), 其他资源必须等待。

死锁、饥饿、活锁

死锁:大家对于一个公共资源进行彼此争夺,又不愿意释放自己资源的时候,那么就形成了死锁

饥饿:线程本身来说是有优先级的,一些线程一直无法充分的获取到资源,就属于饥饿状态,如果程序中发现这种情况,我们需要给予重点的关注和一些补齐

活锁:当某一个线程调度不太智能的时候,就会出现资源处于闲置状态,谁都不愿意占用的情况

上述三种状态,无论哪种状态产生都会出现系统阻塞塞车的情况,一定要有效的避免他,一个最有效的解决死锁,线程资源争抢的事情就是,为每一个线程分配一个属于自己的不被别人影响的资源,这是最理想的做法

image-20220905162340091

线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

线程安全:

优点:可靠

缺点:执行速度慢

使用建议:需要线程共享时使用

线程不安全:

优点:速度快

缺点:可能与预期不符

使用建议:在线程内部使用,无需线程间共享

线程安全三大特性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何 因素打断,要么就都不执行。i = i + 1

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观 察另一个线程,所有的操作都是无序的

创建多线程的三种方式

Java 中创建线程三种方式

1、继承Thread类创建线程

2、实现Runnable接口创建线程

3、使用Callable和Future创建线程

方式一:继承Thread类

public class Match1 {
    public static void main(String[] args) {
        Runner liuxiang = new Runner();   //创建一个新的线程
        liuxiang.setName("刘翔"); //设置线程名称
        Runner feibing = new Runner();
        feibing.setName("飞冰");
        Runner op = new Runner();
        op.setName("路飞");
        
        liuxiang.start();  //启动线程
        feibing.start();
        op.start();

    }
}

class Runner extends Thread{
    public void run(){
        Integer speed = new Random().nextInt(100);
        for (int i = 1; i <= 100; i++){
            try {
                Thread.sleep(1000);     //当前线程休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //this.getName()打印当前线程的名字
            System.out.println(this.getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)");
        }
    }
}

方式二:实现Runnable接口

public class Match2 {
    public static void main(String[] args) {
        Runner2 liuxiang = new Runner2();
        Thread thread1 = new Thread(liuxiang);
        thread1.setName("刘翔");

        Thread feibing = new Thread(new Runner2());
        feibing.setName("飞冰");

        thread1.start();
        feibing.start();

    }
}

class Runner2 implements Runnable{
    public void run(){
        Integer speed = new Random().nextInt(100);
        for (int i = 1; i <= 100; i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)");
        }
    }
}

方式三:使用Callable和Future创建线程(推荐方案)

public class Match3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程池。里面天生有3个“空”线程。Executors是调度器,对线程池进行管理
        ExecutorService executorService =  Executors.newFixedThreadPool(3);
        Runner3 liuxiang = new Runner3();//实例化Callable对象
        liuxiang.setName("刘翔");
        Runner3 feibing = new Runner3();
        feibing.setName("飞冰");
        Runner3 op = new Runner3();
        op.setName("路飞");

        //将这个对象扔到线程池中,线程池自动分配一个线程来运行liuxiang这个对象的call方法
        //Future用于接受线程内部call方法的返回值
        Future<Integer> result1 =  executorService.submit(liuxiang);
        Future<Integer> result2 =  executorService.submit(feibing);
        Future<Integer> result3 =  executorService.submit(op);

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();//关闭线程池释放所有资源
        System.out.println("刘翔累计跑了" + result1.get() + "米" );
        System.out.println("飞冰累计跑了" + result2.get() + "米" );
        System.out.println("路飞累计跑了" + result3.get() + "米" );
    }
}
class Runner3 implements Callable<Integer> {
    private String name ;
    public void setName(String name){
        this.name = name;
    }
    //实现Callable接口可以允许我们的线程返回值或抛出异常
    @Override
    public Integer call() throws Exception {
        Integer speed = new Random().nextInt(100);
        Integer distince = 0; //总共奔跑的距离
        for(int i = 1 ; i <= 100 ; i++){
            Thread.sleep(10);
            distince = i * speed;
            System.out.println(this.name + "已前进" + distince + "米(" + speed + "米/秒)");
        }
        return distince;
    }
}

创建线程的三种方式对比

继承Thread 实现Runnable 利用线程池
优点 编程简单,执行效率高 面向接口编程,执行效率高 容器管理线程,允许返回值与异常
缺点 单继承 无法对线程组有效控制,没有返回值、异常 执行效率相对低,编程麻烦
使用场景 不推荐使用 简单的多线程程序 企业级应用,推荐使用

线程的生命周期

Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:

五种状态

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

image-20220905164540148

1.新建

当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()。

2.就绪

但是当线程对象调用了start()方法之后,就不一样了,线程就从新建状态转为就绪状态。JVM会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至于什么时候被调度,取决于JVM里线程调度器的调度。

注意:

程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。

3.运行

如果处于就绪状态的线程获得了CPU资源时,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU核心,在任何时刻只有一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行(Parallel)执行。

当然,美好的时光总是短暂的,而且CPU讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。

4.阻塞

当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态:

  • 线程调用了sleep()方法,主动放弃所占用的CPU资源;
  • 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
  • 线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify);
  • 线程执行过程中,同步监视器调用了wait(time)
  • 线程执行过程中,遇到了其他线程对象的加塞(join);
  • 线程被调用suspend方法被挂起(已过时,因为容易发生死锁);

当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它:

  • 线程的sleep()时间到;
  • 线程成功获得了同步监视器;
  • 线程等到了通知(notify);
  • 线程wait的时间到了
  • 加塞的线程结束了;
  • 被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);

5.死亡

线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:

  • run()方法执行完成,线程正常结束
  • 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
  • 直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)

转载请注明:西门飞冰的博客 » JAVA-多线程

喜欢 (0)or分享 (0)