Java中的多线程机制读书笔记


人们通常把线程称为轻量级的进程,不过我认为这种说法略有些欠妥,首先很重要的一点,进程是操作系统级别的一个概念,而线程通常是应用程序级别的概念。而且按照Think in Java上所说,并不是所有的多任务操作系统都是支持多线程的,所以这也导致了Java中多线程机制会产生很大的不确定性。除此之外,我认为还有一点很重要,那就是进程之间进行数据的交换,通信通常都比线程之间数据交换通信难很多,文件,数据库等手段比较常用,刚才突然想到本机上的多个进程通过网络端口也可以进行数据交换,这种方法应该效率比较高的。人们通常把二者在一起讨论,通常是因为二者都产生了多个任务同时进行运算的效果。当然,这也是它们被发明出来的原因,至于多进程和多线程的历史渊源,这里就不赘述了。

无论是线程还是进程,其具体的实现都是和操作系统以及CUP芯片有关的。对于多核的CUP,有时候会真的做到多个任务在同时运行,但是对于单核的CPU,那只能是通过CUP在不同的任务之间不断的调度来实现。它不算是真正意义上的并发执行,但是由于CPU运行的速度飞快,往往是感觉不到这个调度的。通常情况下,CPU对于线程之间的调度所消耗的资源是远远小于对进程之间进行调度所消耗的资源的。估计这就是人们把线程称为轻量级的进程的原因。

Java 语言本身支持多线程,这为应用程序的编写提供了很大的方便。Java是执行在一个对上拥有统一接口的虚拟机上的,这本身对于多线程程序的跨平台提供了极大的遍历。但是Java中多线程的内部实现机制却是依赖于底层的。在支持多线程的操作系统上,Java虚拟机会把线程直接映射到操作系统层上,而对于极少数不支持多线程的操作系统,Java虚拟机则自己编写多线程的实现。而且在不同的操作系统上,多线程程序在优先级等问题上处理的态度也不一样,这些导致了多线程程序实际在移植时会产生很大的不确定性。这些是在编写程序过程中应当注意的。

多线程程序的执行效率

多线程程序会因为出现多个线程的调度而消耗而外的资源,所以看起来多线程的执行效率会比单线程低。但是通常程序中会出现很多耗时的操作,比如IO,文件的读写,等待网络等等,由于这些操作的运行速率明显低于CPU的执行效率,所以执行这些操作的时候,CPU基本上是处于一个闲置的状态,这是对时间资源的很大浪费。如果一个IO操作与后面的代码并没有很大的关系,那么便可以讲这个IO操作放入一个单独的线程中去执行。这样原来程序运行的时间是说有操作的时间和,那么现在就变成了一个木桶效应,程序的运行时间就是所有操作中最耗时的操作而已。举个例子,比如需要将一大批数据写入文件,这个操作往往是与以后的代码无关的,那么便大可讲这段代码放在一个单独的线程中,不必等文件的写入操作完成在执行以后的步骤。如此,只要合理的使用多线程,多线程的效率会比单线程高很多的。

Java中实现多线程最基本的有两种方式:

一、通过继承 Thread 类并实现 Runnable 接口。

二、实现Runnable 接口,然后通过一个Thread类启动线程。

无论哪种方式,很明显的可以看出Java中实现多线程重点在于Runnable接口。Runnable接口下有一个抽象方法 public void run() ; 该方法是线程的入口方法。但是Runnable本身并不能实现多线程,真正来实现多线程的是Thread类。多线程必须通过Thread类中的start() 方法来启动。应用程序可以直接去调用Runnable 中的run方法,但是这种做法和普通函数调用没有任何区别,方法还是在原来的线程中执行的。

这里想提一下android中的Handler 。这个类在android应用程序中使用的很多但是它并非多线程。Handler 虽然还是需要一个Runnable 来执行任务,但是Handler只是在自己所在的线程中调用run方法而已。所以android的开发教程中往往会提示不要把太复杂的代码放到Handler中间去执行。它不能实现真的多线程。

多线程的创建与执行代码这里就不赘述了~~这里提一下多线程创建过程中的栈。每个线程都会有一个入口函数,虽然从实际上说有的方法在本质上没有任何区别,但是有些方法因为是一个线程执行过程中的第一个方法,而且通常这个方法执行完毕后这个线程就进入死亡状态了。平常我们经常遇到的main方法其实就是一个入口方法。每个线程都会维护一个自己的线程栈,这个栈中存有线程的执行信息,我个人认为线程间的调度也就是通过这个线程栈实现的。Thread 中调用start方法会产生一个新的线程栈,但是在当前线程中调用run方法是不会产生一个新的线程栈的,run方法还是在当前线程栈中执行的,而start方法会在创建线程栈后把Thread中的run方法调入这个新的线程栈。这就是一个新的线程的创建过程。

线程的生命周期:

任何一本说线程的书都会告诉你线程的以下几个状态。产生、就绪,运行(活跃状态)、阻塞、死亡。当Thread类被创建时,变有一个线程产生。Start方法使一个新建的线程进入就绪状态,就绪状态的线程是可以运行的。当然仅仅是可以,也许CPU的时间片没有转到它,所以说start方法被调用后线程不一定会立即开始执行。

运行,有时候又称为活跃状态。顾名思义,就是当前正在占领着CPU执行计算任务的线程,这样的线程是活跃的。一个活跃的线程可以转变为阻塞状态,就绪状态,死亡状态。由活跃线程转变为就绪状态,通常有俩种情况。一、线程的时间周期结束,系统强制停止当前线程,调入下一个线程使用CPU资源。二、线程自己让出了CPU资源让别的线程使用CPU资源。在Java中,通过yield() ; 方法会使当前线程进入就绪状态,让出CPU的使用权。要注意的是,线程让出CPU使用权不一定是进入就绪状态,也有可能是进入阻塞状态。只有处于活跃状态的线程才能使用CPU资源。

死亡状态。线程执行完毕自动进入死亡状态。或者强行终止线程。调用stop方法可以强行终止线程,但是Java最新的标准中已经建议不要使用这个方法了。所以要终止线程得采用别的方法。

必须注意的是,一个死亡的线程是不能再次运行的,线程运行结束后就会变成垃圾被回收。这也是与androidhandler不一样的地方,handler中可以通过postremoverunnable终止或重新执行。这也说明正好handler并不是真的多线程。

阻塞状态。这个时候往往是因为线程等待IO操作,或者调用了waitsleep等方法而处于阻塞状态。要想结束阻塞状态必须要等引起阻塞的原因解除。阻塞状态解除后线程进入就绪状态,等待自己的时间片到来进入活跃状态。

线程之间的同步

线程之间同步产生的原因主要是因为对复杂数据的操作必须由多步完成。比如修改某个数据之前得判断。如果多个线程同时进行这几步操作,可能会因为线程切换的原因导致了这几步操作不是在同一个时间片内完成的,一系列关系紧密的操作被分割开来,可能导致其他线程在中间修改了关键数据,因此导致对数据操作的出错。这类错误通常都会发生在多个线程的共享数据之间,线程的私有数据不会发生这种情况,因为私有数据都会有自己的备份。

导致这类问题的原因既然是因为联系紧密的操作被分隔,那么解决方法当然就是让这些操作不可分隔。该方法称为线程同步,或者叫线程加锁。

很多人误以为线程加锁是针对数据的,实际上加锁针对的是数据的操作。因此加锁的部分是操作数据的代码。加锁的机制是用一把锁监视一段代码,当有某个线程执行这段代码时,锁便将这段代码锁住,直到这段代码执行完毕才解锁。被加锁的代码只能由一个线程执行,就是那个引发加锁事件的线程。这实际上说,某段代码实际上只能由一个线程执行,虽然在执行的过程中这个线程有可能失去时间片进入就绪状态,但是获得时间片的线程依旧不能执行这段代码,除非持有代码执行权的线程把这段代码执行完毕,解开代码锁。

这种设计保证了即使时间上某些联系紧密的代码可能被分隔了,但是从逻辑上,他们是一个原子操作,是不可分隔的。

Java当中使用synchronized关键字来加锁代码。有两种方式:一、加锁整个方法。二、仅加锁某一段代码。无论用哪一种方法来加锁,都得使用一个对象来充当锁。Java中每个对象内部都有一个线程锁。当加锁整个方法时会默认使用this充当线程锁,但是加锁某段代码时必须明确指明采用哪个对象来加锁。

个人觉得这种做法是面向对象中封装性的一个体现。因为类的方法通常是和对象本身的数据操纵有关,所以可以用this对象来为其加锁。线程调用对象的成员方法时,自然而然的会用数据所在的对象来加锁。而当对代码块加锁时,应当选择与这段代码所操纵的数据对象作为锁。这样可以很好的封装数据与数据的操纵。是面向对象设计中封装性在多线程条件下的体现。

至于静态方法是属于类本身的,无法用对象来加锁。于是Java中使用类本身的一个描述性对class对象来加锁,这个对象是用来描述类本身的一些属性的。方便Java系统本身管理类的。貌似暑假在做android程序时遇到过。Java中的反射机制应该也与此对象有关。

线程之间的协作

有时候多个线程之间需要在时序上,或逻辑上协调完成某个任务,Java提供了线程之间协作的机制。通过waitnotifynotifyall三个方法来实现。应当注意的是这三个方法来自Object类,每一个java类中都有这三个方法。这点体现了Java本身从一开始就打算支持多线程。这三个方法必须在同步语句内调用。

每一个对象都有一个线程锁,同时都有一个线程队列。线程间协作方法只在同步区被调用。当调用wait方法时,执行这段代码的线程就会进入阻塞状态,并且被加入到当前线程锁对象的等待队列中。当调用notify方法时,就会从当前线程锁对象等待队列中取出一个线程,并进入就绪状态。而notifyall方法则会唤醒当前对象锁等待队列中所有的线程,使其进入就绪状态。

线程的终止

由于stop方法可能会引起线程间同步的问题,所以java已经将该方法废弃。所以目前终止一个运行的线程得用一些别的方法。

通常一个线程都是处于一个死循环里面,因此可以用一个标准变量来处理,当准备终止该进程时,只需修改该标准变量,使run方法结束即可。

对于有些不是死循环的线程,我还没有好方法终止它………………。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值