线程的生命周期
新建
- 当程序new了一个线程后,该线程就处于新建状态。
- 此时该线程仅仅由java虚拟机为其分配内存,并初始化其成员变量,并未表现出线程的动态特征,程序也不会执行其线程体。
就绪
- 当线程对象调用其start方法后,该线程就处于就绪状态。
- 此时只是表示该线程可以运行了,并未真正开始运行,至于该线程何时开始运行则取决于JVM里线程调度器的调度。
运行
- 如果处于就绪状态的线程获得了CPU,开始执行线程体,则该线程处于运行状态。
阻塞
当发生如下情况时,线程将进入阻塞状态。
- 线程调用sleep方法
- 线程调用了一个阻塞式的IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有(与线程锁有关)
- 线程在等待某个通知(notify)
- 调用了suspend方法将线程挂起(该方法容易造成死锁,应该尽量避免使用该方法)
重新进入就绪状态
被阻塞的线程在合适的时候会重新进入就绪状态(注意是就绪状态而不是运行状态),针对上面的几种状况,线程重新进入就绪状态的条件分别为
- 调用sleep方法的线程经过了指定时间
- 线程调用的阻塞式IO方法已经返回
- 线程成功获得了试图取得的同步监视器
- 线程正在等待某个通知时,其他线程发出了一个通知
- 调用了resume方法恢复被挂起的线程
死亡
线程会以如下三种方式结束,结束后线程就处于死亡状态。
- 线程体执行完毕,线程正常结束
- 线程抛出一个未捕获的Exception或Error
- 直接调用该线程的stop方法结束该线程(该方法容易造成死锁,通常不推荐使用)
如何判断线程是否死亡
可以通过线程的isAlive方法判断,当线程处于就绪、运行、阻塞状态时,返回true;当线程处于新建、死亡状态时,返回false。
线程状态转换图
控制线程
join线程
- 当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join方法加入的线程执行完为止。
后台线程
- 后台线程是在后台运行的,为其他的线程提供服务。
- 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
- 如何创建:通过调用Thread的setDaemon(true)方法可以将指定线程设置为后台线程,注意该方法要在start方法前调用。
- 主线程默认是前台线程,如果没有使用setDaemon(true)方法显示设置,那么前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
线程睡眠:sleep
- Thread.sleep()方法:让当前正在执行的线程暂停一段时间,并进入阻塞状态。
线程让步:yield
- Thread.yield()方法:与sleep方法不同,该方法只是将该线程转入就绪状态。完全可能出现的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
- 当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级高于当前线程的处于就绪状态的线程才会获得执行的机会。
关于sleep方法和yield方法的区别
- sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield方法只会给优先级相同,或优先级更高的线程执行机会
- sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield方法不会将线程转入阻塞状态,它只是强制当前线程转入就绪状态,因此完全有可能某个线程调用了yield方法暂停之后,立即再次获得处理器资源而被执行。
- sleep方法声明抛出了InterruptedException异常,而yield方法没有抛出异常。
- sleep方法比yield方法有更好的可移植性,通常不建议使用yield方法来控制并发线程的执行。
线程同步
线程安全问题
例如银行取钱问题,在某种情况下,可能出现两个取钱线程同时操作同一个账户的情况(虽然出现的可能性很小,但还是有可能会发生),这时假如两个线程都是取钱800,而账户余额只剩下1000,那么就可能出现取了两次800的情况而出现余额-600的情况,显然现实中是不允许这种事情发生的。
解决方法
同步代码块
1 | synchronized (obj) { |
- 上面的obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,而任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成时,该线程才会释放对该同步监视器的锁定。
- 虽然java程序允许使用任何对象作为同步监视器,但同步监视器的目的就是阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源作为同步监视器。
- 在银行取钱问题中,可以把账户作为同步监视器,取钱过程作为同步代码块,这样就保证只有一个线程能够进行取钱操作,当一个线程的取钱操作结束后另一个线程才能取得同步监视器进行另外的取钱操作。
同步方法
1 | public synchronized void draw() { |
- 上面的方法用synchronized修饰,是一个同步方法。对于同步方法,无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
- 因为draw方法是一个同步方法,而该方法是账户类的一个方法,所以对于同一个账户变量来说,如果有多个线程操作该变量,那么同一时刻只能有一个线程能执行draw方法,其他线程执行draw方法时会阻塞,这样就保证了取钱操作的安全性。
注意
- 可变类的线程安全是以降低程序的运行效率为代价的,为了减少线程安全带来的负面影响,程序可采取如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步。
- 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本来保证性能,在多线程环境中使用线程安全版本。例如JDK提供的StringBuilder和StringBuffer,在单线程环境下使用StringBuilder来保证较好的性能,在多线程环境中使用StringBuffer来保证安全。
释放同步监视器的锁定
何时释放
- 当前线程的同步方法、同步代码块执行完毕
- 当前线程在执行同步方法、同步代码块时出现了未处理的Error或Exception而异常结束
- 当前线程执行同步方法或同步代码块时,程序执行了同步监视器对象的wait方法,则当前线程暂停,并释放同步监视器
以下情况不会释放
- 当前线程执行同步方法或同步代码块时,程序调用了sleep、yield方法来暂停当前线程,当前线程不会释放同步监视器
- 当前线程执行同步代码块时,其他线程调用了当前线程的suspend方法将当前线程挂起,当前线程不会释放同步监视器。当然,程序应该尽量避免使用suspend和resume方法来控制线程。
关于synchronized关键字
同步锁
Lock
- Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的condition对象。
ReentrantLock(可重入锁)
- Lock接口的实现类
基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class X {
//创建锁对象
private final ReentrantLock reentrantLock = new ReentrantLock();
public void method() {
//加锁
reentrantLock.lock();
try {
//需要保证线程安全的代码
} finally {
//使用finally块来保证释放锁
reentrantLock.unlock();
}
}
}何时使用:需要实现ReentrantLock的独有功能时
公平锁和非公平锁
公平锁的创建:
1
private ReentrantLock fairLock = new ReentrantLock(true);
非公平锁的创建:
1
private ReentrantLock unFairLock = new ReentrantLock();
区别:公平锁指的是线程获取锁的顺序是按照加锁顺序来的,即先加锁的线程(执行lock方法)会优先获得锁,非公平锁指的是抢锁机制,先lock的线程不一定先获得锁。
- 非公平锁可能会产生饥饿现象,公平锁虽然不会产生饥饿现象,但是性能会比非公平锁差很多。
Lock与synchronized对比
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
ReentrantLock和synchronized对比
- 可重入性:从名字上理解,ReenTrantLock的字面意思就是可重入锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。(lock了几次相对应就要unLock几次)
- 锁的实现:Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,两者的区别就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
- 性能上:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized。
- 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
可重入锁和不可重入锁的区别
参考
- Java:重入锁ReentrantLock详解、代码实战、与Synchronized对比
- java多线程系列(四)—ReentrantLock的使用
- ReenTrantLock可重入锁(和synchronized的区别)总结
- ReentrantLock 的使用
死锁
简介
- 当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应采取措施避免死锁发生。
- 一旦出现死锁,整个程序既不会发生任何异常,也不会给任何提示,只是所以线程处于阻塞状态,无法继续。
简单例子
思路是创建两个字符串a和b,再创建两个线程A和B,让每个线程都用synchronized锁住字符串(A先锁a,再去锁b;B先锁b,再锁a),如果A锁住a,B锁住b,A就没办法锁住b,B也没办法锁住a,这时就陷入了死锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public class DeadLock {
static final String obj1 = "obj1";
static final String obj2 = "obj2";
public static void main(String[] args){
new Thread(new Lock1()).start();
new Thread(new Lock2()).start();
}
}
class Lock1 implements Runnable{
public void run(){
try{
System.out.println("Lock1 running");
while(true){
synchronized(DeadLock.obj1){
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
synchronized(DeadLock.obj2){
System.out.println("Lock1 lock obj2");
//不会执行到这句,因为obj2锁已经被Lock2持有,而Lock2需要获取obj1锁,
//但obj1锁已经被Lock1持有,所以Lock2也不能获取到obj1锁
//造成的后果是:Lock1和Lock2都在等待对方执行完代码后释放自己所需的锁,
//但双方的代码都需要持有已经被对方持有的锁,所以就形成一个死循环。
//双方都在等待对方释放锁,程序也就变成了虽然没有报错,但运行不下去的情况。
//这就是死锁
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class Lock2 implements Runnable{
public void run(){
try{
System.out.println("Lock2 running");
while(true){
synchronized(DeadLock.obj2){
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
synchronized(DeadLock.obj1){
System.out.println("Lock2 lock obj1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
输出结果:1
2
3
4Lock1 running
Lock1 lock obj1
Lock2 running
Lock2 lock obj2
可以看出,最终程序没有报错,但却停在了这里运行不下去,两个线程都变成了阻塞状态,形成了是死锁。