创建线程的正确姿势

多线程核心知识概括:

简介:多线程,作为实现软件并发执行的一个重要方法,也是越来越重要。多线程可以在时间片里被cpu快速切换,所以资源能更好被调用、程序设计在某些情况下更简单、程序响应更快。本系列博客主要介绍多线程的核心知识,对多线程核心知识进行归纳总结,本博客暂时不涉及并发包和线程池的内容,希望能对阅读本博客的人有所帮助,如有不足之处欢迎留言指正

进程

所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中的程序,并且具有一定独立的功能,进程是系统进行资源分配和调度的一个独立单位.

多线程

多线程扩展了多进程的概念,使得同一进程可以同时并发处理多个任务.线程也被称为轻量级进程,线程时进程的执行单元.线程在程序中是独立的并发的执行流.当进程被初始化之后,主线程就被创建了.

线程是进程的组成部分,一个进程可以有多个线程,但一个线程必须有一个父进程.线程可以拥有自己的栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源.因为多个线程共享父进程的所有资源,因此编程比较方便,但必须更加小心,需要确保线程不会妨碍到同一进程里的其他线程.

线程是独立运行的,它并不知道进程中是否还有其他的线程存在.线程的执行是抢占式的:当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行.

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.

从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序可以有多个执行部分同时进行,但操作系统无须将多个线程看做多个独立的应用,对多线程实现调度和管理以及资源分配.线程的调度和管理由进程本身负责完成.

总结:

1.操作系统可以同时执行多个任务,每个任务就是进程;

2.进程可以同时执行多个任务,每个任务就是线程.

多线程的优势

1.进程之间不能共享内存,但线程之间共享内存很容易

2.系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高.

3.Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程.

多线程的应用是很广泛的,比如一个浏览器必须能同时下载多个图片,一个web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收…

线程的创建和启动

Java使用Thread类代表线程,每个线程对象都必须是Thread类或其子类的实例.每个线程的作用是完成一定的任务,实际上是执行一段程序流.

线程执行策略

1. 串行单线程执行
列:在食堂打饭的时候,食堂安排一个打饭大妈打饭,所有同学都排成一个队形,打饭大妈相当于线程,同学打饭相当于任务,这种一个大妈给一群站好队形的同学打饭相当于java中的单线程串行执行任务。

串行执行的缺点很明显,当一个同学选菜慢的情况下,效率低下,后面的同学早已不耐烦了。

2. 并行多线程执行

为了解决上述问题,食堂打算聘请多个打饭大妈,同时给同学打饭,这样的话同学倒是开心了,很快就能吃上饭,但是食堂承受不了了,因为食堂需要开更多的打饭窗口以及给大妈的工资,映射到多线程中也一样,如果每个任务都创建一个线程来处理的话,任务数量一旦多了,内存就会有很大的负担,甚至可能宕机。

3. 线程池

为了解决上述问题,食堂又做了改进,就是先确定好打饭大妈的数量,然后让同学们排队,每个打饭大妈打完一次饭就叫队列中的下一个。映射到java中我们创建指定数量的线程,然后把任务放到一个队列中去处理,当每个线程处理完任务后就会去队列中取下一个任务处理,可以把线程重复利用,因为线程的创建也是消耗资源的。

所以结论就是:在一定范围内增加线程数量的确可以提升系统的处理能力,但是过多的创建线程将会降低系统的处理速度,如果无限制的创建线程,将会使系统崩溃。

实现多线程的方式

1. 从不同的角度看,会有不同的答案典型的答案是两种,Oracle官网文档明确说明2种:第一种是实现runnable接口,第二种是实继承Thread
2从原理分析,其实Thread类实现了Runnable接口,并看Thread的run方法会发现其实两种本质是一样的,run方法的代码如下:

      @Override
       public void run() {
      if (target != null) {
        target.run();
       }

}
3方法一和方法二,也就是“继承Thread类然后重写run()”和实现“Runnable接口并传入Thread类”在实现多线程本质上,并没有区别,都是是调用了start方法来新建线程。两个方法最重要的区别在于run()方法的内容同来源:
方法一:最终调用target.run();而target为Runna接口的实例
方法二:run()方法整个都被重写了
4.还有其他方式实现多线程,例如线程池、定时器,他们也能新建线程,但从源码来看,从没逃过本质,也就是实现Runnable接口和继承Thread类

/**
 * 描述:     定时器创建线程
 */
public class DemoTimmerTask {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}
/**
 * 描述:     线程池创建线程的方法
 */
public class ThreadPool5 {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Task() {
            });
        }
    }
}
class Task implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

5.结论只能通过新建Thread类这种方式来创建线程,但是类里面的run方法有两种方式来实现,第一种是重写run方法,第二种实现Runnable接口的run方法,然后把runnable接口的实例传给Thread类,除此之外表面上线程池、定时器等工具类也可以创建线程,但是本质都逃不出刚才所说的范围

实现Runnable接口和继承Thread类哪种方式比较好?

实现runnable接口比较好

1). 从代码架构角度:具体的任务(run方法)应该和“创建和运行线程的机制(Thread类)”解耦,用runnable对象可以解耦。通过runnable方式实现的run方法中的内容是具体执行的任务,可以让一个单独任务类实现runnable接口,然后把对应的实例传给Thread类,并且任务类也不负责创建线程等工作,是解耦的。同时java不支持多继承,所以导致继承Thread类的子类扩展性差,代码演示如下:

/**
 * 描述:     用Thread方式实现线程
 */
public class ThreadStyle extends Thread{

    @Override
    public void run() {
        System.out.println("用Thread类实现线程");
    }

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

这里我们我们看出,ThreadStyle类继承Thread,创建线程和启动执行线程都是ThreadStyle的实例,不符合单一职责原则,耦合性搞

/**
 * 描述:     用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable{

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

    @Override
    public void run() {
        System.out.println("用Runnable方法实现线程");
    }
}

和继承Thread了开启线程不同,执行和创建线程是解耦的,不同的任务可以传入不同的Runnable实例,更加有灵活性,面向接口编程,符合依赖倒转原则
2).使用继承Thread的方式,那么每次想建立一个新的任务,只能新建一个独立的线程,这样做的损耗比较大(比如从头开始创建一个线程、执行完毕以后再销毁。如果实际工作内容,也就是run函数里只是简单的打印一行文字,那么可能线程实际工作内容还不如消耗大),如果使用runnable和线程池就大大降低了这样的损耗,通常我们选择方法1
3).继承Thread类也有几个小小的好处,相比缺点而言不值一提,比如:
在run方法内获取当前线程可以直接用this,而无须用Thread.currentThread()方法;
继承的类的内部变量不会直接共享,少数不需要共享的变量的场景下使用起来会方便
两种方法的本质区别:
方法一和方法二,也就是“继承Thread类然后重写run()”和实现“Runnable接口并传入Thread类”在实现多线程本质上,并没有区别,都是调用了tart方法来新建线程。两个方法最重要的区别在于run()方法的内容同上文已经交代
如果一个线程同时调用多次会怎么样?
Start()原理解读
含义:1:启动新线程----请求jvm执行,通过线程调度器启动线程
2:准备工作—就绪(上线文、盏等)
3:不能重复调用start方法—否则报错-----非法线程状态

  public synchronized void start() {
       
       if (threadStatus != 0)//每次启动判断状态
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();//本地方法,c++实现
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }

线程只有在new的状态下启动线程或者线程未启动,否则会抛IllegalThreadStateException()异常,详见CanStartTwice10类,满足条件的线程加入线程组,最后调用本地方法start0()
注:由JVM创建的main线程和system线程组,并不是通过start来启动

既然Thread.start()只能调用一次。那么线程池是如何实现线程复用的呢?
原因:线程重用的核心是,线程池对Thread做了包装,不重复调用Thread.start()。而是自己有一个Runnable.run()run方法里面循环在跑,跑的过程中不断检查是否有新加的子runnable对象,有新的runnable进来就调一下run方法,其实就是一个大额run把小的run1/run2…给串联起来,同一个Thread可以执行不同的runnable,主要是线程池把线程和runnable通过BlockingQueue给解耦了,线程可以从Blockingqueue中不断获取新的任务详细分析展开:
在ThreadPoolExecutor的execute方法,作用是添加将要执行的任务

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
start()会调用run()方法让为什么不直接调用run方法呢?
start()是开启一个线程,而直接调用run方法,run方法只是一个普通方法, 和线程生命周期没有任何关系
下篇:如何中断线程:

发布了23 篇原创文章 · 获赞 2 · 访问量 856

猜你喜欢

转载自blog.csdn.net/qq_34800986/article/details/103325244