先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少。线程可以认为是轻量级的进程,充分地利用线程可以使得同一个进程中执行多种任务。Java是第一个在语言层面就支持线程操作的主流编程语言。和进程类似,线程也是各自独立的,有自己的栈,自己的局部变量,自己的程序执行并行路径,但线程的独立性又没有进程那么强,它们共享内存,文件资源,以及其他进程层面的状态等。同一个进程内的多个线程共享同样的内存空间,这也就意味着这些线程可以访问同样的变量和对象,从同一个堆上分配对象。显然,好处是多线程之间可以有效共享很多资源,坏处是要确保不同线程之间不会产生冲突。
每个Java程序都至少有一个线程——main线程。当Java程序开始运行时,JVM就会创建一个main线程,然后在这个main线程里面调用程序的main()方法。JVM同时也会创建一些我们看不到的线程,比如用来做垃圾收集和对象终结的(garbage collection and object finalization,JVM最重要的两种资源回收),或者JVM层面的其他整理工作。
为什么要使用线程?
1、可以使UI(用户界面)更有效(利用多线程技术,可以把时间较长的UI工作交给专门的线程,这样UI的主线程就不会被长期占用,界面就会流畅而不停滞)
2、有效利用多进程系统(单线程+多进程,太浪费系统资源了)
3、简化建模
4、执行异步处理或者后台处理(不同的线程做不同的工作)
线程的生命周期:
通常有两种方法创建一个线程,1、implement Runnable接口,2、继承Thread类
创建完成后,这个线程就进入了New State,直到它的start()方法被调用,它就进入了Runnable状态。
一个线程从Running State进入Terminated / Dead State标志着线程的终结,正常情况下有这么几种可能性:
1、线程的run()执行结束
2、线程抛出没有捕捉到的异常或者错误
当一个Java程序所有的非守护进程(Daemon Thread,即守护进程,负责一些包括资源回收在内的任务,我们无法结束这些进程)结束时,程序宣告执行结束。
Java Thread的重要方法必须熟悉。
join():目标线程结束之前调用线程将会被Block,例如在main线程中创建了一个thread1线程,调用thread1.join(),这就意味着thread1将优先执行,在thread1结束后main thread才会继续。一个join()方法的使用案例:将一个任务(比如从1万个元素的数组中选出最大值)分拆成10个小任务(每个小任务负责1000个)分配给10个线程,调用它们的start(),然后分别调用join(),以确保10个任务都完成(分别选出了各自负责的1000个元素中的最大值)后,主任务再进行下去(从10个结果中挑出最大值)。
sleep():使当前线程进入Waiting State,直到指定的时间到了,或者被其他线程打断,从而回到Runnable State。
wait():使调用线程进入Waiting State,直到被打断,或者时间到,或者被其他线程使用notify(),notifyAll()叫醒。
notify():这个方法被一个对象调用时,如果有多个线程在等待这个对象,这些处于Waiting State的线程中的一个会被叫醒。
notifyAll():这个方法被一个对象调用时,如果有多个线程在等待这个对象,这些处于Waiting State的线程都会被叫醒。
多线程共享资源是讨论最多的话题,也是最容易出问题的地方之一,Java定义了两个关键字,synchronized和volatile,用来帮助共享的变量在多线程情况下能够正常工作。
synchronized一方面确保同一时间内只有一个线程能够执行一段受保护的代码,并且这个线程对数据(变量)进行的改动对于其他线程是可见的。这里包含两层意思:前者依靠lock(锁)来实现,当一个线程处理一段受保护代码时,该线程就拥有lock,只有它释放了这个lock,其他线程才有可能获得并访问这段代码;后者由JVM机制实现,对于受synchronized保护的变量,需要读取时(包括获取lock)会首先废弃缓存(invalidate cache),进而直接读取main memory上的变量,完成改动时(包括释放lock)会flush缓存,强行把所有改动更新到main memory。
为了提高performance,处理器都是会利用缓存来保存一些变量储存在内存中的地址,这样就存在一种可能性,在一个多进程架构中,一个内存地址在一个进程的缓存中被修改了,其他进程并不会自动获得更新,于是不同进程上的2个线程就会看到同一个内存变量的两个不同值(因为两个缓存中的保存的内存地址不同,一个被修改过)。Volatile关键字可以有效地控制原始类型变量(primitive variable,比如integer,boolean)的单一实例:当一个变量被定义为volatile的时候,无论读写,都会绕过缓存而直接对main memory进行操作。
关于Java的锁(Locking)有一个问题需要注意:一段被lock保护的代码并不意味着就一定不能被多线程同时访问,而只意味着不能被等待同一个lock的多线程同时访问。
对于绝大多数的synchronized方法,它的lock就是调用方法的实例对象;对于static synchronized方法,它的lock是定义方法的类(因为static方法是每个类只有一份copy,而不是每个实例都有一份copy)。因此,即使一个方法被synchronized保护了,多线程仍然可以同时调用这个方法,只要它们是调用不同实例上的这个方法。
synchronized代码块稍微复杂一些,一方面它也需要和synchronized方法一样定义lock的类型,另一方面必须考虑如果最小化被保护的代码块,即能不放到synchronized里面就不放进去,比如局部变量的访问通通不需要保护,因为局部变量本身就只存在于单线程上。
下面两种加锁的方法是等效的,都是以Point类的实例为lock(即多线程可以同时访问不同Point实例的synchronized setXY()方法):
死锁(deadlock)是多线程编程中最怕遇到的情况,简单来说就是线程1拥有对象A的lock,等待获取对象B的lock,线程2拥有对象B的lock,等待获取对象A的lock,这样就没完没了了。怎么防止deadlock是一个大话题,可以写一本书,简单来说的话就是当线程需要获取多个lock的时候(比如线程1和2都要获取对象A和B的lock),永远按照一定的次序来。比如如果线程1和2都是先获取对象A的lock,再获取对象B,那就不会出现上面的deadlock了,因为如果1获得了A lock,2就得等,而不是去获得B lock。