Java多线程详解(上)

总结自《Java编程思想》第21章并发(P650~)

1.进程、线程含义

  • 进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配的基本单位,运行在它自己的地址空间内的自包容的程序,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
  • 线程:线程是进程中的一个实体,作为系统调度和分派的基本单位.

1.1线程与进程的比较

引用自《计算机操作系统》第四版,P76

类别 进程 线程
调度基本单位 传统OS中进程是调度的基本单位 引入线程的OS中,线程是调度的基本单位
并发性 进程间可并发执行 进程中的多个线程可并发执行
拥有资源 系统拥有资源的一个基本单位 不拥有系统资源,仅拥有保证独立运行的少量资源
独立性 进程拥有自己独立的地址空间和其它资源,其它进程不可访问 同一进程的不同线程共享进程的内存地址空间和资源
系统开销 创建与撤消进程时,OS需要大量的开销 OS创建开销小,并且上下文切换远比进程快
支持多处理系统 传统单线程进程只能运行在一个处理机上 多线程进程可将进程的多个线程分配到多个处理机上并发执行
通信 管道、FIFO、消息队列、信号量、共享存储、Socket 线程共享同一进程中的内存与资源,可直接通信
状态 创建、就绪、阻塞、执行、终止 新建、就绪、阻塞、死亡

1.2总结

进程与线程都是顺序执行程序,一个线程就是在进程中的一个单一的顺序控制流。进程与线程都是可以解决并发编程的问题,线程是轻量级的进程,线程诞生于进程中。线程的底层机制是切分CPU时间,CPU将轮流给每个任务分配其占用时间

2.线程驱动任务执行

2.1 任务的概念

Java中实现了接口java.lang.Runable的类就可以称之为任务,其实现方法run()中编写的即是需要并发执行的代码。

@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();
}

2.2 线程的简单实现

线程在Java中只有一个类表示,那就是java.lang.Thread,简单启动一个线程的代码如下

fun main(args: Array<String>) {
   println(Thread.currentThread().name)
    val t = Thread(Runnable {
        print("ThreadName = ${Thread.currentThread().name} it's other thread")
    })
    t.start()
}

/*output
main
ThreadName = Thread-0 it's other thread
*/

创建Thread时在其构造函数中传入一个Runable类参数,当线程调用start()方法时,即会驱动Runable中代码段的执行

2.3线程与任务的关系

线程不是任务,这两者的概念一定要区分清楚。Thread类自身不执行任何操作,它只是驱动赋予它的任务。
下面我将用一张图来通俗地讲解一个线程驱动任务运行的过程
在这里插入图片描述

  • 将任务比做一个集装箱,而线程则比做一个无动力货船,这艘货船设定一次只能装载一个集装箱
  • 集装箱要想被运送到目的地,必须得装入货船中。要首先必须要有一个货船,货船是调度的基本单位,它不能自己运行,只能通过动力拖船来将货船拉至目的地码头,而动力拖船也只能拖动货船而不能直接运送集装箱。对应创建一个Thread对象,在构造器内传入一个任务(即Runable对象),这样CPU才能够执行Thread中的任务。
  • 货船装好集装箱后,要做一些被动力船拖运的准备,比如拉起锚,人员就绪,扬帆等,让动力拖船随时都可以来拖动该船。对应Thread实例对象调用start(),表明该线程执行必须的初始化操作,之后线程调度机制即进行该线程的CPU时间片分配,让线程中的任务能被执行。
  • 动力拖船会受调度中心的指挥先拖A船一段时间,再拖B船一段时间,后再拖C船一段时间,最终将三艘船都拖至码头结束三个集装箱的运送任务。对应线程调度让CPU执行线程A中的任务多少时间,B和C各多少时间,这个时间段是不确定的,由线程调度策略决定,让每个线程都会分配至数量合理的时间去驱动它的任务。
  • 在只有一艘动力拖船的情况下,在动力拖船拉货船至目的码头的过程中,具体某一时刻只能有一艘货船在被拖动。对应单CPU环境中,某一时刻只能有一个线程在执行,虽然表面看上去任务是并发执行的,实际上依然是顺序执行的。在多核CPU的环境中,线程会被分配至不同的CPU中执行,实现真正意义上的任务并发执行。Java线程调度的机制是抢占式的,所以CPU会周期性地中断线程,将上下文切换到另一个线程,为每个线程提供时间片。

3.线程的多种实现方式

3.1 自定义类继承自Thread类

3.1.1 传统方式

继承类通用重写thread中的run方法,然后生成实例调用start(),即可运行一个线程

3.1.2 聚合方式

在传统方式中将start()的调用放至自定义类中的构造方法中执行,从而在生成实例时即开始运行一个线程。

实例如下

class SimpleThread : Thread{
    private var countDown = 5
    companion object {
        private var  threadcount = 0
    }
    constructor():this(Integer.toString(++threadcount))
    constructor(name: String): super(name)
    init {
        start() //聚合方式
    }
    
    override fun toString(): String {
        return "#$name($countDown)"
    }

    override fun run() {
        while (true){
            print(this)
            if(--countDown == 0) return
        }
    }
}


fun main(args: Array<String>) {
    for(i in 1..5){
        SimpleThread()
        //SimpleThread().start() 传统方式启动线程
    }

}

/*output 输出结果是不确定的,有各种可能
#2(5)#2(4)#1(5)#2(3)#3(5)#2(2)#1(4)#1(3)#4(5)#2(1)#5(5)#3(4)#5(4)#4(4)#1(2)#4(3)#5(3)#3(3)#5(2)#4(2)#1(1)#4(1)#5(1)#3(2)#3(1)
or
1(5)#2(5)#3(5)#1(4)#2(4)#1(3)#3(4)#1(2)#4(5)#2(3)#4(4)#1(1)#3(3)#4(3)#2(2)#4(2)#3(2)#4(1)#2(1)#3(1)#5(5)#5(4)#5(3)#5(2)#5(1)
*/

3.2 实现Runable接口

3.2.1 传统方式

自定义类实现Runable接口,重写run方法,然后创建一个Thread类对象,在构造器中传入一个Runable的对象,最后调用Thread对象的start()运行线程,如2.2代码所示

3.2.2 聚合方式

在自定义类中持有一个Thread对象,然后在构造器中调用Thread#start()运行线程,即实现一个自管理的Runable.

实例如下:

class SelfManaged: Runnable{

    private var countDown = 5
    private var t = Thread(this)
    init {
        t.start()
    }
    override fun toString(): String {
        return "${Thread.currentThread().name}($countDown)"
    }
    override fun run() {
        while (true){
            print(this)
            if(--countDown == 0){
                return
            }
        }
    }
}

fun main(args: Array<String>) {
    for(i in 1..5){
        SelfManaged()
    }
}
/*output
Thread-1(5)Thread-2(5)Thread-0(5)Thread-2(4)Thread-1(4)Thread-2(3)Thread-0(4)Thread-2(2)Thread-1(3) Thread-2(1)Thread-0(3)Thread-1(2)Thread-0(2)Thread-1(1)Thread-0(1)Thread-3(5)Thread-3(4)Thread-3(3) Thread-3(2)Thread-3(1)Thread-4(5)Thread-4(4)Thread-4(3)Thread-4(2)Thread-4(1)
*/

3.3 通过内部类来将线程代码隐藏在类中,即是在类中再放置一个上述内部线程类用以实现线程并启动。

class InnerThread(name: String){
    private var countDown = 5
    private var inner1:Inner
    init {
        inner1 = Inner(name)
    }
    private inner class  Inner : Thread{
        constructor(name:String): super(name){
            start()
        }

        override fun run() {
            try {
                while (true){
                    println(this)
                    if(--countDown == 0 ) return
                    sleep(10)
                }
            }catch (e:InterruptedException){

            }
        }

        override fun toString(): String {
            return "$name:$countDown"
        }
    }
}

fun main(args: Array<String>) {
    InnerThread("InnerThread")
}

/*output
InnerThread:5
InnerThread:4
InnerThread:3
InnerThread:2
InnerThread:1
*/

4.Thread类简单分析

4.1 实现了Runable接口

Thread实现了Runable接口,所以可通过继承Thread重写run方法的方式来实现多线程。

public class Thread implements Runnable {}

4.2 线程名称

  • 线程名字是Thread的一个标识,用下述字段表示
   private volatile char  name[];
  • 通过setName()与getName()进行名称的设置与获取。
  • 通过静态方法Thread.currentThread()来获取当前运行的线程对象
  • Thread重写了toString(),输出的内容为Thread[线程名称,线程优先级,线程组名]
 public String toString() {
        ThreadGroup group = getThreadGroup();
        if (group != null) {
            return "Thread[" + getName() + "," + getPriority() + "," +
                           group.getName() + "]";
        } else {
            return "Thread[" + getName() + "," + getPriority() + "," +
                            "" + "]";
        }
    }

4.3 优先级

  • Thread类中用以表示优先级的字段
private int            priority;
  • 通过setPriority()、getPriority()进行线程优先级的设置与获取。
  • Java中的线程优先级一共有10级,与多数操作系统都不能映射地很好,所以调整优先级时一般只使用MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY这三种级别,默认是NORM_PRIORITY = 5
  • 优先级高的会让调度器倾向先执行,优先级低的线程并不会得不到执行,只是执行的频率较低,一般不建议操纵线程优先级

4.4 后台(daemon)线程

  • 后台线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这些线程并不属于程序中不可或缺的部分。
  • 因此当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。
  • 反过来说,只要有任何非后台线程还在运行,程序就不会终止。执行main()的就是一个非后台线程。
  • 由后台线程所创建的任何线程都将被自动设置成后台线程。

实例如下:

class SimpleDaemons: Runnable{
    override fun run() {
        try {
            while (true){
                TimeUnit.MILLISECONDS.sleep(100)
                println("${Thread.currentThread()} $this")
            }
        }catch (e: InterruptedException){
            println("sleep() interrupted")
        }
    }
}

fun main(args: Array<String>) {
    for(i in 0 until 10){
        val daemon = Thread(SimpleDaemons())
        daemon.isDaemon = true //设置线程为后台线程,必须在Start之前调用
        daemon.start()
    }
    println("All daemons started")
    TimeUnit.MILLISECONDS.sleep(99) //当睡眠时间为99ms时小于子线程的睡眠时间100ms时,所有后台子线程都不会执行
}

/**output
99ms    
All daemons started
100ms 有可能执行全部或部分子线程,也可能都不执行
All daemons started
Thread[Thread-8,5,main] mutilthread.SimpleDaemons@1857af2f
Thread[Thread-9,5,main] mutilthread.SimpleDaemons@2afd6006
Thread[Thread-7,5,main] mutilthread.SimpleDaemons@4c630b0d
Thread[Thread-6,5,main] mutilthread.SimpleDaemons@61e0162
Thread[Thread-5,5,main] mutilthread.SimpleDaemons@21f189d9
Thread[Thread-4,5,main] mutilthread.SimpleDaemons@488a7f84
*/

结论是当main线程停止执行时程序退出,所有的后台线程都跟着一起终止不会得到执行

发布了92 篇原创文章 · 获赞 68 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/pigdreams/article/details/83247159