Java多线程开发(一)Java多线程编程简介

参考

【Java并发系列01】Thread及ThreadGroup杂谈
Java中interrupt的使用
Java 线程状态之 RUNNABLE
锁和监视器之间的区别 – Java并发
Java并发编程实战:第一章、第十章
Java多线程编程实战指南(核心篇):第一章、第二章

多线程编程对于现在的开发人员来说是一种很常见的技术了,特别是对于我这样的Android程序员,“UI操作要在主线程,耗时操作要在子线程“是开始学习Android的时候就要记住的原则。但是多线程代码要比单线程开发更复杂,还存在一些单线程编程时不会遇到的问题。这篇博客主要是介绍多线程开发的基础:Thread类的API 和 多线程代码可能存在的问题。这是我们多线程开发的基础,下一节我会基于这篇内容,继续讲述Java为了存在的问题而提供给我们的解决工具。

Java线程简介

对于Java平台来说,创建一个线程就是创建一个Thread类实例,java.lang.Thread 类是Java平台对线程的实现。

Thread类构造方法和属性

Thread有很多的构造方法,最后都会调到同一个初始化方法:private void init(ThreadGroup g, Runnable target, String name, long stackSize),这个方法的参数用于设置Thread的几个属性,它们的含义和作用方便是:

  • g:ThreadGroup类型, 线程分组,用于对一组线程进行一些批量操作,默认值是父线程(即创建该线程的线程)所属分组。
  • target:Runnable类型, 实现线程的业务逻辑,在run()方法中被调用。
  • name:String类型,作为标识,用于打印之类的时候区分线程。默认值格式为Thread-线程编号
  • stackSize:long类型,代表这个线程预期的堆栈大小,不指定默认为0,表示由平台决定。

除了上面的属性,线程还有几个比较重要的属性

  • priority:int类型,这个属性可以通过setPriority(int newPriority)方法设置,这个属性是给线程调度器的提示,表示程序希望哪个线程得到更多的运行机会(但不意味着一定能得到)。priority的取值范围为0~10,默认值是父线程的priority
  • daemon:boolean类型,这个属性可以通过setDaemon(boolean on)设置(只能在start()方法调用之前调用,即线程运行之前调用,否则会报错),daemon表示该线程是否为守护线程(守护线程不会阻止虚拟机的关闭),默认值是父线程的daemon

常用Thread类方法

  • static Thread currentThread() :返回当前线程(即当前代码的执行线程)。
  • static void sleep(long millis):使当前线程释放CPU,休眠一段时间。
  • void start():启动线程(线程启动的过程是异步的,方法返回不代表新线程已经开始运行),这个方法只能被调用一次。
  • void run():用于执行任务逻辑,一般由虚拟机而非应用程序主动调用。
  • void interrupt():用于中断线程,这个方法的效果有两种情况:
    • 如果这个线程正被阻塞,它可以迅速中断被阻塞的线程,并抛出InterruptedException异常。
    • 如果线程没有被阻塞,它就只是设置一个中断标志,而不能阻止线程的继续运行。
  • static boolean interrupted():判断当前线程是否已有中断标志并清除掉中断标志(即之后的代码将不会知道线程曾经被中断过,除非interrupt()再次调用)。
  • boolean isInterrupted():判断当前线程是否已有中断标志,这个方法不会清除中断标志。

线程的生命周期

一个线程在其创建、启动到运行结束的整个生命周期中会经历若干状态,这些状态由枚举类Thread.State定义,共有6种,它们分别是:

  • NEW:线程创建之后,启动之前处于这个状态。因为线程只能被启动一次,所以线程只会处于这个状态一次。
  • RUNNABLE:这个状态可以当做两个子状态:READYRUNNING
    • READY :这个状态的线程被称为活跃线程,表示已经准备好由线程调度器调度转换为RUNNING态。
    • RUNNING:表示状态正在运行。
  • BLOCKED:线程被阻塞(发起一个阻塞式I/O操作或者申请被持有的资源时),线程从 RUNNING变为这个状态,这个状态下的线程不会占用CPU,当导致线程阻塞的操作完成时,线程重新回到RUNNING态。
  • WAITING:线程等待其他线程执行另外特定操作而进入该状态,同样会释放CPU。进入这个状态的方法有:Object.wait()Thread.join()LockSupport,park();对应的唤醒方法是: Object.notify()Object.notifyAll()LockSupport.unpark(thread)
  • TIMED_WAITING:和WAITING类型,差别在这个状态下的线程不会像WAITING态的线程一样无限制的等下去,在指定时间内没有等到期待的特定操作时,这个状态下的线程会自动转换为RUNNING态。
  • TERMINATED:已经结束执行的线程处于这个状态,和 NEW同理,一个线程也只会处于这个状态一次。

我看的书上这段说的状态划分原则不是很清晰,综合了一些博客和SDK里Thread类的注释,我现在的理解是:

  • 处于RUNNABLE态的线程区别只在于是否拥有CPU资源:即正在执行和在等待执行,因为CPU会根据时间分片来轮转调度,所以这两个子状态的线程会随着进出调度队列而在很短时间内(毫秒级)切换子状态,从而区分的价值不大。
  • BLOCKEDWAITING同样会暂停执行,同样会释放CPU。两者的区别在于BLOCKED态的线程等待的是监视器锁,当竞争锁成功之后,这个线程就会转换为RUNNABLE态;而 WAITING态的线程是主动暂停运行的,不涉及到锁,并且需要其他线程调用特定方法来唤醒这个线程,否则这个线程永远不会执行(TIMED_WAITING则会等待指定时间后醒来)。

线程整个运行和状态转换规律如下图:
线程生命周期

多线程编程的优势和风险

多线程有充分利用CPU、提高系统的吞吐量、更快速响应用户UI操作等操作等等优势毋庸赘言,但多线程开发相较于单线程开发也更加复杂,同时也多了很多在单线程开发时不会遇到的问题。下面我总结了多线程带来的风险。

安全性问题

用《Java多线程编程实战指南》中的观点,宽泛地描述多线程的安全性问题就是:

如果一个类在单线程环境下能够运作正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全(Thread-safe)的,相应地我们称这个类具有线程安全性(Thread Safety)。反之,如果这个类在单线程环境下运作正常而在多线程环境下则无法正常运作,那么这个类就是非线程安全的。

为什么单线程环境下可以正常运行的代码放在多线程环境下运行就会出问题呢? 对于同时运行的多个线程来说,如果每个线程的功能和使用的资源都不涉及到其他线程,那么每个线程就是互不影响的,当然不会存在问题,但是这种情况很少。一般而言,多个线程线程之间会存在对某些资源的共享,对应到代码层面上,就是共享变量的使用。由于Java内存模型和数据缓存的影响,每个线程内部的操作和造成的结果对外部的其他线程都不是立即可见的。由此导致程序出错。出错的原因表现为3个方面:原子性、可见性和有序性。

  • 原子性:出于我们本意,应该是不可分割的一个操作(这个操作要么没有执行,要么执行了)被其他线程在其中插入了其他操作,导致状态错误。
  • 可见性:一个线程更新了状态不能被另一个线程立即看到,导致另一个线程读取到了旧状态或者重复更新覆盖掉了这次更新。
  • 有序性:出于优化的目的,编译器和处理器会对代码指令调整顺序,这种调整的规则只保证单线程内是正确的,而不考虑多线程。比如类a进行操作A,C;类b进行操作依赖于A的D,于是b通过判断a是否已经完成了C来决定是否进行操作D,单线程下这个思路是正确的,但多线程下,指令重排序下a的操作A,C的顺序就可能发生改变,导致C操作完成于A之前,以至于D操作失败。

活跃性问题

安全性问题是保证“程序运行时不会发生错误”,而活跃性则关注的是“程序会正常的结束运行”。由于缺少某些资源或程序本身缺陷导致程序无法继续下一步操作时,就是发生了活跃性问题,单线程环境下,程序进行无限循环就是一种问题的形式,而在多线程环境下,活性问题大多由线程交互导致:

  • 死锁:两个线程同时以不同的顺序来获得不同锁,导致各自持有一个锁,而请求对方持有的锁,导致程序无法继续运行。
  • 锁死:如果有一个线程调用了wait方法,但是一直没有其他线程调用notify方法或者是在它调用wait之前调用了notify导致这个线程收不到唤醒信号而一直挂起,这种情况就被称为锁死。
  • 饥饿:修改某个线程优先级导致这个线程一直得不到CPU调用。
  • 活锁:线程申请资源一直不成功,或者其他原因,导致线程一直重复执行相同的操作。

性能问题

与活跃性问题密切相关的是性能问题。活跃性意味着程序会正常运行得到结果,但是不保证效率,因为我们还希望程序的性能,即运行的速度越快越好。多线程编程的性能过,可能由很多原因导致:

  • 频繁的切换线程导致的上下文切换,导致CPU花费过多时间用于线程调度。
  • 多个线程共享数据为了线程安全必须使用同步机制,从而带了额外的性能开销。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值