Java多线程Thread详细讲解(万字教程)

一、线程的基本概念

  线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

    好处 :(1)易于调度。

               (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

               (3)开销少。创建线程比创建进程要快,所需开销很少。。

               (4)利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行

二、进程与线程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

多进程是指操作系统能同时运行多个任务(程序)。

多线程是指在同一程序中有多个顺序流在执行。

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。

在Java中,一个应用程序可以包含多个线程。每个线程执行特定的任务,并可与其他线程并发执行多线程使系统的空转时间最少,提高CPU利用率、多线程编程环境用方便的模型隐藏CPU在任务间切换的事实在Java程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。

主线程的重要性体现在两个方面:

1、它是产生其他子线程的线程。

2、通常它必须最后完成执行,因为它执行各种关闭动作。

 三、进程与线程的区别:

    (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

    (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

    (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.

    (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

 四、同步和互斥的区别:

当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。

      所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

        所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

 五、进程间通信的方式?

(1)管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

    (2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。

    (3)消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。

    (4)共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

    (5)信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。

    (6)套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

 六、进程和线程的关系:

  (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

    (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。

    (3)处理机分给线程,即真正在处理机上运行的是线程。

    (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.

 七、多线程的优点

  • 使用线程可以把占据时间长的程序中的任务放到后台去处理
  • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
  • 程序的运行速度可能加快
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
  • 多线程技术在IOS软件开发中也有举足轻重的位置。

八、多线程的缺点

  • 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
  • 更多的线程需要更多的内存空间。
  • 线程可能会给程序带来更多“bug”,因此要小心使用。
  • 线程的中止需要考虑其对程序运行的影响。
  • 通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。

总结:

线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。

线程是java独有的吗,操作系统本身有没有线程?

当然有啦!

操作系统里面会有很多个程序,比如QQ,你打开QQ,QQ又会有很多个线程。所以,线程可不是java独有的东西啊。

在JVM的设定中Java的线程和操作系统的线程是一一对应的:

  Java如何开启线程? 方式1:继承Thread类

告诉你这个秘密,Java开启线程只有一种方式,就是Thread类的start方法!

JDK看似提供了很多和多线程相关的类,可实际上有且仅有Thread类能通过start0()方法向操作系统申请线程资源(本地方法)。

下面我们演示一下如何用Java开启一个线程。

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了!");
    }


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

你也可以直接new一个Thread类,并且临时重写run方法(少写一个类)

public static void main(String[] args) {
    //new MyThread().start();
    new Thread(){
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行了!");
        }
    }.start();
}

Java如何开启线程? 方式2:实现Runnable接口!(详细讲解)

这种方式的本质还是第一种,即Thread类实例调用start方法,从而去调用本地线程资源。因为有且仅有Thread类能通过start0()方法向操作系统申请线程资源(本地方法)!

​JVM对于操作系统来说就是一个程序,是一个进程,JVM进程中可以有很多个线程,我们调用start方法其实就是去申请这个线程资源。

Runnable接口:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

看上面的注释就知道怎么用了,当一个对象实现Runnable接口,你就去创建一个Thread,启动这个Thread,会导致run方法被调用。

我们按照官方的指导来做:

public class MyThread02 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了!");
    }


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

看到了吧,这种方式其实还是第一种。不同点在于,第一种方式是直接在Thread子类中重写的Run方法,而这种方式是你自己弄个类,里面有个run方法,传到Thread类中去,最后在Thread类启动的时候,会去调用那个类的run方法。换句话说,如果没有Thread类,这个Runnable实现类是没法自己跑起来的

所有的秘密就在Thread类的构造函数里面。

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

调了init方法

private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
    init(g, target, name, stackSize, null);
}

又是重载,继续找:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
    //省略其他代码...
    this.target = target;
}

原来,传入的Runnable对象是放到属性target中了。

/* What will be run. */
private Runnable target;

我们再看下Thread类中的run方法:

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

如果是方式1,这个run方法被重写了,启动start方法的时候就直接调用那个被重写后的run方法了。现在使用方式2的话,就还是调用Thread原生的run方法,做了一个中介,又去调用target的run方法,而target是我们自己传进去的,相当于把具体run的逻辑写到外部一个类里面去了。

以上就是第二种方式的原理,下面介绍一下如何简写代码?

比如,我们可以直接把Runnable实现类用匿名的方式new出来(少些一个类):

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行了!");
        }
    }).start();
}

因为Runnable接口只有一个抽象方法,所以还可以直接用Lamda表达式:

new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + "执行了!");
}).start();

如果还不明白原理,可以看下这张图,安排的明明白白啦。

 Java如何开启线程? 方式3:用线程池Executor

什么是线程池:  java.util.concurrent.Executors 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。

一个线程池包括以下四个基本组成部分:
                1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
                2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
                3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
                4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池的作用:

线程池作用就是限制系统中执行线程的数量。
     根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

ExecutorService

真正的线程池接口。

ScheduledExecutorService

能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor

ExecutorService的默认实现。

ScheduledThreadPoolExecutor

继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

1. newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3. newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

4.newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

为了方便演示,我们就创建只有一个线程的线程池:

package com.javaxbfs.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyThread03 implements Runnable{

    public static void main(String[] args) throws InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //提交线程任务-Runnable对象
        Future<?> future = executorService.submit(new MyThread03());

        while(!future.isDone()){
           Thread.sleep(200);
        }

        // 关闭线程池
        executorService.shutdown();
        System.out.println("线程池关闭");
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "========>正在执行!");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "========>执行成功!");
    }
}

效果:

pool-1-thread-1========>正在执行!
pool-1-thread-1========>执行成功!
线程池关闭

这边我们使用的是Runnable对象,线程池还可以接受Callable对象。

package com.javaxbfs.thread;

import java.util.concurrent.*;

public class MyThread04 implements Callable {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //提交线程任务-Runnable对象
        Future<String> future = executorService.submit(new MyThread04());

        while(!future.isDone()){
           Thread.sleep(200);
        }
        String result = future.get();
        System.out.println("这里拿到线程任务的返回值:" + result);
        // 关闭线程池
        executorService.shutdown();
        System.out.println("线程池关闭");
    }

    @Override
    public String call() {
        System.out.println(Thread.currentThread().getName() + "========>正在执行!");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "========>执行成功!");
        return "SUCCESS";
    }
}

 

效果:

pool-1-thread-1========>正在执行!
pool-1-thread-1========>执行成功!
这里拿到线程任务的返回值:SUCCESS
线程池关闭

两种方式我都写了例子,关于这个后面还会细说。

 总结

1. 不管你是用上面介绍的三种方法的哪一种,都绕不开Thread类,因为只有Thread类的start方法可以去申请本地线程资源!

2. 用Runnable可以把任务代码写在Thread类外面,降低耦合度,更加灵活。

3.Callable一般配合线程池使用,重写的call方法可以添加返回值。

4.线程执行入口永远是Thread类的run方法,这个run方法要么被你重写,要么调用target的run方法,target就是实现runnable接口的对象。

 

 封装Thread类的start方法(经典技巧)

 

Thread类的start方法是去请求本地资源的,很多源码喜欢把这个细节封装掉,现在让我们来模仿一下这种技巧。

package com.javaxbfs.thread;

public class MyThread05 implements Runnable{

    public void begin(){
        new Thread(this).start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了!");
    }


    public static void main(String[] args) {
        MyThread05 myThread05 = new MyThread05();
        myThread05.begin();
    }
}

思路特别简单,不需要你手动去new Thread类了,而是直接在Runnable类中添加begin方法,把这个事情给做了。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>