Java高级深度解析之(三)——线程的Java实现方式解读

前言:

这一节的主要内容是学习和总结Java中线程的实现和应用。在我们开发的过程中或多或少的都会和线程打交道。在IT小伙伴吹牛皮的时候也会聊到类似的东西。因此,好好了解和学习线程还是很有必要的。

首先复习一下上一节(Java高级深度解析之(二)——线程和进程的Java实现方式)中讲到的知识点;上一节主要讲解了线程的基本含义及特征,还阅读了Java中操作进程实现方式的相关源码。最后说了Java中进程的两种创建方式,它们分别是:其一是通过ProcessBuilder类来创建进程; 其二是通过Runtime的exec方法来创建进程。如果有兴趣的小伙伴可以去看看。话不多说,咱们一起来看看Java中是如何操作线程的吧。

线程的基本概念

线程的引入

如果说,在操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。为了说明这一点,我们首先来回顾进程的两个基本属性:①进程是一个可拥有资源的独立单位;②进程同时又是一个可独立调度和分派的基本单位。正是由于进程有这两个基本属性,才使之成为一个能独立运行的基本单位,从而也就构成了进程并发执行的基础。然而,为使程序能并发执行,系统还必须进行以下的一系列操作。

创建进程

系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O 设备,以及建立相应的 PCB

撤消进程

系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消 PCB

进程切换

对进程进行切换时,由于要保留当前进程的CPU环境和设置新选中进程的CPU环境,因而须花费不少的处理机时间。

换言之,由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。正因如此,在系统中所设置的进程,其数目不宜过多,进程切换的频率也不宜过高,这也就限制了并发程度的进一步提高。

如何能使多个程序更好地并发执行同时又尽量减少系统的开销,已成为近年来设计操作系统时所追求的重要目标。有不少研究操作系统的学者们想到,若能将进程的上述两个属性分开,由操作系统分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以做到“轻装上阵”;而对于拥有资源的基本单位,又不对之进行频繁的切换。

正是在这种思想的指导下,形成了线程的概念。

随着VLSI技术和计算机体系结构的发展,出现了对称多处理机(SMP)计算机系统。它为提高计算机的运行速度和系统吞吐量提供了良好的硬件基础。但要使多个CPU很好地协调运行,充分发挥它们的并行处理能力,以提高系统性能,还必须配置性能良好的多处理机OS。但利用传统的进程概念和设计方法,已难以设计出适合于SMP结构的计算机系统的oS。这是因为进程“太重”,致使实现多处理机环境下的进程调度、分派和切换时,都需花费较大的时间和空间开销。如果在OS中引入线程,以线程作为调度和分派的基本单位,则可以有效地改善多处理机系统的性能。因此,一些主要的OS(UNIX、OS/2、Windows)厂家都又进一步对线程技术做了开发,使之适用于SMP的计算机系统。

线程与进程的比较

线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process)或进 程元,相应地把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线 程的任务。在引入了线程的操作系统中,通常一个进程都拥有若干个线程,至少也有一个线程。下面我们从调度性、并发性、系统开销和拥有资源等方面对线程和进程进行比较。

调度

在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源 拥有的基本单位,把传统进程的两个属性分开,使线程基本上不拥有资源,这样线程便能 轻装前进,从而可显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程 的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。

并发性

在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线 程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源 的利用率和系统的吞吐量。例如,在一个未引入线程的单 CPU 操作系统中,若仅设置一个文件服务进程,当该进程由于某种原因而被阻塞时,便没有其它的文件服务进程来提供服务。在引入线程的操作系统中,则可以在一个文件服务进程中设置多个服务线程。当第一个线程等待时,文件服务进程中的第二个线程可以继续运行,以提供文件服务;当第二个 线程阻塞时,则可由第三个继续执行,提供服务。显然,这样的方法可以显著地提高文件服务的质量和系统的吞吐量。

拥有资源

不论是传统的操作系统,还是引入了线程的操作系统,进程都可以拥有资源,是系统中拥有资源的一个基本单位。一般而言,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源, 如已打开的文件、I/O 设备等,可以供该进程中的所有线程所共享。

系统开销

在创建或撤消进程时,系统都要为之创建和回收进程控制块,分配或回收资源,如内 存空间和 I/O 设备等,操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地, 在进程切换时,涉及到当前进程 CPU 环境的保存及新被调度运行进程的 CPU 环境的设置, 而线程的切换则仅需保存和设置少量寄存器内容,不涉及存储器管理方面的操作,所以就 切换代价而言,进程也是远高于线程的。此外,由于一个进程中的多个线程具有相同的地 址空间,在同步和通信的实现方面线程也比进程容易。在一些操作系统中,线程的切换、 同步和通信都无须操作系统内核的干预。

线程的属性

在多线程OS 中,通常是在一个进程中包括多个线程,每个线程都是作为利用 CPU的 基本单位,是花费最小开销的实体。线程具有下述属性。

轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、 能保证其独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器,保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

独立调度和分派的基本单位

在多线程 OS 中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。

可并发执行

在一个进程中的多个线程之间可以并发执行,甚至允许在一个进程中 的所有线程都能并发执行;同样,不同进程中的线程也能并发执行。

共享进程资源

在同一进程中的各个线程都可以共享该进程所拥有的资源,这首先表现在所有线程都具有相同的地址空间(进程的地址空间)。这意味着线程可以访问该地址空 间中的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等

线程的状态

状态参数

OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项:

  • 寄存器状态,它包括程序计数器PC和堆栈指针中的内容;
  • 堆栈,在堆栈中通常保存有局部变量和返回地址;
  • 线程运行状态,用于描述线程正 处于何种运行状态;
  • 优先级,描述线程执行的优先程度;
  • 线程专有存储器,用于保 存线程自己的局部变量拷贝;
  • 信号屏蔽,即对某些信号加以屏蔽

线程运行状态

如同传统的进程一样,在各线程之间也存在着共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。相应地,线程在运行时也具有下述三种基本状态:

  • 执行状态,表示线程正获得处理机而运行;

就绪状态,指线程已具备了各种 执行条件,一旦获得 CPU 便可执行的状态;

  •  阻塞状态,指线程在执行中因某事件而受阻,处于暂停执行时的状态

线程的创建和终止

在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”。它可根据需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。

如同进程一样,线程也是具有生命期的。终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。但有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。在太多数的OS中、线程被中止后并不立即释放它所占有的盗源、只有当进程中的其它线程执行了分离函数后,被终止的线程才与盗源分离,此时的资源才能被其它线程利用。

虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。

多线程OS中的进程

在多线程 OS 中,进程是作为拥有系统资源的基本单位,通常的进程都包含多个线程并 为它们提供资源,但此时的进程就不再作为一个执行的实体。多线程 OS 中的进程有以下属性。

作为系统资源分配的单位

在多线程OS 中,仍是将进程作为系统资源分配的基本单位,在任一进程中所拥有的资源包括受到分别保护的用户地址空间、用于实现进程间和线程间同步和通信的机制、已打开的文件和已申请到的 I/O 设备,以 及一 张 由 核心 进程维护的地址映射表,该表用于实现用户程序的逻辑地址到其内存物理地址的映射。

可包括多个线程

通常,一个进程都含有多个相对独立的线程,其数目可多可少, 但至少也要有一个线程,由进程为这些(个)线程提供资源及运行环境,使这些线程可并发执行。在 OS 中的所有线程都只能属于某一个特定进程。

进程不是一个可执行的实体

在多线程 OS 中,是把线程作为独立运行的基本单位, 所以此时的进程已不再是一个可执行的实体。虽然如此,进程仍具有与执行相关的状态。 例如,所谓进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的与进程状态有关的操作,也对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;又如,在把某进程激活时,属于该进程的所有线程也都将被激活。

线程间的同步和通信

为使系统中的多线程能有条不紊地运行,在系统中必须提供用于实现线程间同步和通 信的机制。为了支持不同频率的交互操作和不同程度的并行性,在多线程 OS 中通常提供多 种同步机制,如互斥锁、条件变量、计数信号量以及多读、单写锁等。

互斥锁(mutex)

互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁lock操作用于将mutex关上,开锁操作unlock则用于打开mutex。

当一个线程需要读/写一个共享数据段时,线程首先应为该数据段所设置的mutex执行关锁命令。命令首先判别mutex的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;而如果mutex是处于开锁状态,则将mutex关上后便去读/写该数据段。在线程完成对数据的读/写后,必须再发出开锁命令将mutex打开,同时还须将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待mutex打开的队列上。

另外,为了减少线程被阻塞的机会,在有的系统中还提供了一种用于mutex上的操作命令Trylocke当一个线程在利用Trylock命令去访问mutex时,若mutex处于开锁状态,Trylock将返回一个指示成功的状态码;反之,若mutex处于关锁状态,则Trylock并不会阻塞该线程,而只是返回一个指示操作失败的状态码。

条件变量

在许多情况下,只利用 mutex 来实现互斥访问可能会引起死锁,我们通过一个例子来 说明这一点。有一个线程在对 mutex 1 执行关锁操作成功后,便进入一临界区 C,若在临界 区内该线程又须访问某个临界资源 R,同样也为 R 设置另一互斥锁 mutex 2。假如资源 R 此 时正处于忙碌状态,线程在对 mutex 2 执行关锁操作后必将被阻塞,这样将使 mutex 1 一直保 持 关 锁 状态;如果保持了资源 R 的线程也要求进入临界区 C,但由于 mutex 1 一直保持关 锁状态而无法进入临界区,这样便形成了死锁。为了解决这个问题便引入了条件变量。

每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着 一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条 件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。

现在,我们看看如何利用互斥锁和条件变量来实现对资源 R 的访问。线程首先对 mutex 执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解 资源的情况。只要发现所需资源 R 正处于忙碌状态,线程便转为等待状态,并对 mutex 执 行开锁操作后,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对 mutex 执行开锁操作。下面给出了对上述资源的申请(左 半部分)和释放(右半部分)操作的描述。

原来占有资源 R 的线程在使用完该资源后,便按照右半部分的描述释放该资源,其中 的 wakeup(condition variable)表示去唤醒在指定条件变量上等待的一个或多个线程。在大多 数情况下,由于所释放的是临界资源,此时所唤醒的只能是在条件变量上等待的某一个线 程,其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件,该文件允许 多个线程同时对它执行读操作。在这种情况下,当一个写线程完成写操作并释放该文件后, 如果此时在该条件变量上还有多个读线程在等待,则该线程可以唤醒所有的等待线程。

信号量机制

前面所介绍的用于实现进程同步的最常用工具——信号量机制,也可用于多线程 OS 中,实现诸线程或进程之间的同步。为了提高效率,可为线程和进程分别设置相应的信号量。

私用信号量(private samephore)

当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的 命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。私用信号量属于特 定的进程所有,OS 并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常 结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为 0() 也不能将它传送给下一个请求它的线程

公用信号量(public semaphort)

公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有 着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受 保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量。如果信号量 的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。可见,公用信号量是一种比较安全的同步机制

线程的实现方式

线程已在许多系统中实现,但各系统的实现方式并不完全相同。在有的系统中,特别 是一些数据库管理系统如 Infomix,所实现的是用户级线程(UserLevel Threads);而另一些系 统(如 Macintosh和 OS/2 操作系统)所实现的是内核支持线程(KernelSupported Threads); 还 有一些系统如 Solaris 操作系统,则同时实现了这两种类型的线程。

内核支持线程

对于通常的进程,无论是系统进程还是用户进程,进程的创建、撤消,以及要求由系统设备完成的I/O操作,都是利用系统调用而进入内核,再由内核中的相应处理程序予以完成的。进程的切换同样是在内核的支持下实现的。因此我们说,不论什么进程,它们都是在操作系统内核的支持下运行的,是与内核紧密相关的。

这里所谓的内核支持线程KSTKernel Supported Threads),也都同样是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等也是依靠内核,在内核空间实现的。此外,在内核空间还为每一个内核支持线程设置了一个线程控制块,内核是根据该控制块而感知某线程的存在,并对其加以控制。

 

这种线程实现方式主要有如下四个优点:

(1) 在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;

(2) 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运 行,也可以运行其它进程中的线程;

(3) 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;

(4) 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。

内核支持线程的主要缺点是:对于用户的线程切换而言,其模式切换的开销较大,在 同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。

用户级线程

用户级线程ULT(User Level Threads)仅存在于用户空间中对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。可见,这种线程是与内核无关的。我们可以为一个应用程序建立多个用户级线程。在一个系统中的用户级线程的数目可以达到数百个至数千个。由于这些线程的任务控制块都是设置在用户空间,而线程所执行的操作也无须内核的帮助,因而内核完全不知道用户级线程的存在。

值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程而言似乎是公平的。但假如在进程A中包含了一个用户级线程,而在另一个进程B中含有100个用户级线程,这样,进程A中线程的运行时间将是进程B中各线程运行时间的100倍;相应地,其速度要快上100倍。

假如系统中设置的是内核支持线程,则调度便是以线程为单位进行的。在采用轮转法调度时,是各个线程轮流执行一个时间片。同样假定进程 A 中只有一个内核支持线程,而 在进程 B 中有 100 个内核支持线程。此时进程 B 可以获得的 CPU 时间是进程 A 的 100 倍, 且进程 B 可使 100 个系统调用并发工作。

使用用户级线程方式有许多优点,主要表现在如下三个方面:

(1)线程切换不需要转换到内核空间,对一个进程而言,其所有线程的管理数据结构均在该进程的用户空间中,管理线程切换的线程库也在用户地址空间运行。因此,进程不必切换到内核方式来做线程管理,从而节省了模式切换的开销,也节省了内核的宝贵资源。

(2)调度算法可以是进程专用的。在不干扰操作系统调度的情况下,不同的进程可以根据自身需要,选择不同的调度算法对自己的线程进行管理和调度,而与操作系统的低级调度算法是无关的。

(3)用户级线程的实现与操作系统平台无关,因为对于线程管理的代码是在用户程序内的,属于用户程序的一部分,所有的应用程序都可以对之进行共享。因此,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。用户级线程实现方式的主要缺点在于如下两个方面:

(1)系统调用的阻塞问题。在基于进程机制的操作系统中,大多数系统调用将阻塞进程,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。

(2)在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点。内核每次分配给一个进程的仅有一个CPU,因此进程中仅有一个线程能执行,在该线程放弃CPU之前,其它线程只能等待。

组合方式

有些操作系统把用户级线程和内核支持线程两种方式进行组合,提供了组合方式 ULT/KST 线程。在 组 合 方式线程系统中,内核支持多 KST 线程的建立、调度和管理,同时, 也允许用户应用程序建立、调度和管理用户级线程。一些内核支持线程对应多个用户级线 程,程序员可按应用需要和机器配置对内核支持线程数目进行调整,以达到较好的效果。 组合方式线程中,同一个进程内的多个线程可以同时在多处理器上并行执行,而且在阻塞 一个线程时,并不需要将整个进程阻塞。所以,组合方式多线程机制能够结合 KST 和 ULT 两者的优点,并克服了其各自的不足。

理论知识总结

以上从不同的角度学习总结了线程的概念,西安城的同步和通信机制以及线程的实现方式阐述了。以下的篇幅用于讲解Java8中线程的源码解读和线程的创建和使用。

Java实现创建线程和多线程的实现

1、通过实现Thread类实现多线程;

 

在Java操作线程的过程中,总是有人说有两种,四种,六种实现方式,但是通常主流的实现方式无非就是两种。其一是继承Thread类,其次就是实现Java的Runnable接口了。但是可以有其它的衍生实现方式。比如还有Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。在这里主要先讲一下普遍意义的多线程实现,即继承Thread类和实现Runnable接口。然后再扩展以下其它的实现方式。

Thread类简介:

Thread类API解读

Thread类是Java中实现多线程的主要方式之一。

Thread的构造器:

Thread的源码解读:

从图中可以看出Thread类提供了不同的构造方式用于创建线程。有带参数的和无参数的。再来看一下Thread类的具体实现方式是怎样的。

以上图片说明Thread类它实现了Runnable接口并且实现了Runnable接口的run()方法以实现多线程的。

综上所述,创建一个线程,只需要让自己的类继承Thread类即可。简单代码如下: 

package test1;

public class MyThread extends Thread {
    public void run() {
        System.out.println("Run my Thread...");
    }
}

代码调用:

package test1;

public class ThreadImp {
    public static void main(String[] args) {
        testThread1();
    }

    /**
     * Java多线程实现案例1: 继承Thread的实现
     */
    public static void testThread1(){

        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        m1.start();
        m2.start();
        System.out.println("m1线程的名称:"+m1.getName()+"\nm1线程的ID"+m1.getId());
    }
}

以上就是通过继承Thread类实现线程的基本方式。

 

此文未完,还会继续更新...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值