[Java基础] Java线程复习笔记
先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少。线程可以认为是轻量级的进 程,充分地利用线程可以使得同一个进程中执行多种任务。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()叫醒。
wait和sleep有一个非常重要的区别是,一个线程sleep的时候不会释放任何lock,而wait的时候会释放该对象上的lock。
notify():这个方法被一个对象调用时,如果有多个线程在等待这个对象,这些处于Waiting State的线程中的一个会被叫醒。
notifyAll():这个方法被一个对象调用时,如果有多个线程在等待这个对象,这些处于Waiting State的线程都会被叫醒。
多线程共享资源是讨论最多的话题,也是最容易出问题的地方之一,Java定义了两个关键字,synchronized和volatile,用来帮助共享的变量在多线程情况下能够正常工作。
synchronized一方面确保同一时间内只有一个线程能够执行一段受保护的代码,并且这个线程对数据(变量)进行的改动对于其他线程是可见 的。这里包含两层意思:前者依靠lock(锁)来实现,当一个线程处理一段受保护代码时,该线程就拥有lock,只有它释放了这个lock,其他线程才有 可能获得并访问这段代码;后者由JVM机制实现,对于受synchronized保护的变量,需要读取时(包括获取lock)会首先废弃缓存 (invalidate cache),进而直接读取main memory上的变量,完成改动时(包括释放lock)会flush缓存中的write operation,强行把所有改动更新到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()方法):
public class Point { public synchronized void setXY(int x, int y) { this.x = x; this.y = y; } }
public class Point { public void setXY(int x, int y) { synchronized (this) { this.x = x; this.y = y; } } }
死锁(deadlock)是多线程编程中最怕遇到的情况。什么是死锁?当2个或2个以上的线程因为等待彼此释放lock而处于无限的等待状态就称 为死锁。简单来说就是线程1拥有对象A的lock,等待获取对象B的lock,线程2拥有对象B的lock,等待获取对象A的lock,这样就没完没了 了。
如何检测deadlock?
检查代码,看是否有层叠的synchronized代码块,或者调用彼此的synchronized方法,或者试图获取多个对象上的lock,等等。如果程序员不注意的话,这些情况都容易导致deadlock。
怎么防止deadlock是一个大话题,可以写一本书,简单来说的话就是当线程需要获取多个lock的时候(比如线程1和2都要获取对象A和B的 lock),永远按照一定的次序来。比如如果线程1和2都是先获取对象A的lock,再获取对象B,那就不会出现上面的deadlock了,因为如果1获 得了A lock,2就得等,而不是去获得B lock。
总结一下synchronized关键字的一些注意点:
1、synchronized关键字确保了需要同一个lock的多线程永远无法同时或并行访问同一个共享资源或者synchronized方法
2、synchronized关键字只能修饰方法或者代码块
3、任何时候一个线程想要访问synchronized方法或者代码块时,都要先获取lock,任何时候一个线程结束访问synchronized方法或代码块时,都会释放lock。即使因为错误或异常结束访问,也会释放lock
4、Java线程进入一个实例层synchronized方法时,要先获取对象层面的lock(object level lock);进入静态synchronized方法时,要先获取类层面的lock(class level lock)
5、一个Java synchronized方法调用另一个synchronized方法,两个方法需要同一个lock的时候,线程不需要重新获取lock
6、在synchronized(myInstance)中,如果myInstance为Null,会抛出NullPointerException
7、synchronized关键字一个主要缺点就是它不支持并行的读取(因此对于一些值不可变的情况不要使用这个关键字,否则会无谓地影响performance)
8、synchronized关键字还有一个限制,它只支持单一JVM内的共享资源访问,对于多JVM共享一些文件资源或者数据库资源的时候,单单使用它就不够了,这时候程序员需要实现全局性的lock
9、synchronized关键字对performance影响很大,因此只有当真正需要的时候才用
10、优先使用synchronized代码块,而不是synchronized方法,确保将synchronized代码减小到最精,能不synchronized就不用synchronized关键字
11、静态和非静态的synchronized方法可能同时或者并行运行,因为它们被认为是使用了不同的lock(一个是object level,一个是class level)
12、从Java 5开始,对于volatile修饰的变量,读和写都被保证是原子的(atomic),即安全的。从performance的角度,操作volatile变量比从synchronized代码中访问变量要高效
13、synchronized代码可能会导致死锁
14、Java不允许在构造函数中使用synchronized关键字。理由很简单,如果构造函数中出现synchronized关键字,那当一个线程在构造实例时,其他线程都不知道,这就违背了同步的原则
15、synchronized关键字不能用于修饰变量,正如volatile关键字不能用于修饰方法
16、Java.util.concurrent.locks包提供了synchronized关键字的扩展功能,可以帮助程序员编写更为复杂的多线程操作
17、synchronized关键字同步内存(线程内存和主内存)
18、Java synchronization的一些关键方法,比如wait()、notify()、notifyAll(),定义在Object类中
19、在synchronized代码块中不要以非final变量(non final field)为锁,因为非final变量的引用常常会改变,一旦锁改变了,那synchronization就失去了意义。比如这个例子,一旦对 String变量进行操作,就在内存中生成新的String对象
private String lock = new String("lock"); synchronized(lock){ System.out.println("locking on :" + lock); }
20、不推荐使用String对象作为synchronized代码块的锁,即使是final String。因为String存放在内存的String变量池中,可能会有其他代码或者第三方的代码使用了同一个String对象为锁,这样容易导致一 些无法预测的问题。在下面的例子中,与其使用LOCK为锁,还不如创建一个Object实例为锁。
private static final String LOCK = "lock"; //not recommended private static final Object OBJ_LOCK = new Object(); //better public void process() { synchronized(LOCK) { ........ } }
21、在Java库中,很多类默认不是线程安全的,需要程序员特别注意加上安全保护,比如Calendar, SimpleDateFormat等。