Java 多线程二、线程的生命周期、线程安全、死锁
一、 线程的生命周期
线程是存在生命周期的,线程从创建之后,运行后执行完相关操作,其终点一定是死亡。
如下图:演示线程的生命周期:
线程的生命中期分为五个阶段
- 1.新建
- 2.就绪
- 3.运行
- 4.阻塞(不一定有)
- 5.死亡
这5个阶段里,其中阻塞是不一定有的,其他几个状态都有,线程的最终结果都是死亡。
1.正常状态的变化:
- 新建->就绪:
调用thread.start()
方法即可让线程到就绪状态。 - 就绪->运行:
当线程获取CPU
的执行权时,即会到运行状态。 - 运行->就绪:
当运线程行时,失去CPU
的执行权,即会到就绪状态,如使用yeild()
方法强制切换CPU
的执行权。 - 运行->死亡:
线程的最终结果都应该是死亡,从运行到死亡是不可逆的。
通过执行stop()
方法,或者是抛异常(Error
/Exception
)没有处理的情况下,则该线程会死亡。
2.有阻塞状态的变化
-
运行->阻塞
通过sleep(time)
方法调用,让线程等待一段时间,此时线程会到阻塞状态,直到sleep时间到。
通过wait()
让线程挂起,直到有notify()
通知该线程重新到就绪状态。 -
阻塞->就绪
首先明确,阻塞不能直接到运行状态,线程阻塞后,当线程被唤醒时,他是先到就绪状态,当该线程拿到CPU 的执行权时,放可到运行状态。- 1).当
sleep
时间到时,会从阻塞到就绪 - 2).当被
wait
的线程,使用notify/notifyAll
方法唤醒线程时,被wait的线程会到就绪状态。
- 1).当
二、 线程的安全问题
当多线程之间有共享数据的时候,就会存在线程安全问题。
多线程导致错票问题
- 1.卖票过程出现了重票,错票问题,即出现了线程安全问题。
- 2.出现重票和错票的原因,是因为在一个线程还没操作完,另一个线程过来再操作共享数据,即会出现线程安全问题。
例如:
三个线程同时抢100张票,当加了sleep 之后,出现重票,错票的概率大大增加了,因为在上一个线程还没进行ticket--时候,下一个线程也进来了,导致因为共享数据问题导致错票。
代码如下:
public class WindowTest {
public static void main(String[] args) {
Window win = new Window();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
运行结果中有票号为-1
,即错票。
如何解决?
思路:当一个线程在操作共享数据时,当前线程锁定资源,其他线程不能操作该共享数据,这种情况即使该线程阻塞,其他线程也不允许操作共享数据。
三种方式来解决线程安全问题
1.同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
- 什么是需要被同步的代码?即操作共享数据的代码
- 什么是共享数据?多个线程共同操作的变量。比如该例子中的ticket
- 什么是同步监视器?形象的说就是“锁”。任何一个类的对象,都可以充当锁,但切记锁是惟一的。
切记:多个线程公用一把锁,锁是唯一的。
说明:加了同步监视器之后,在同步代码块中,智能有一个线程操作,其他线程等待,所以这块相当于单线程的,所以效率比较低一些。
在使用继承方式来创建多线程时,要慎用this来充当同步监视器,考虑使用当前类充当同步监视器,如 : Window2.class
例如:
如下方式,使用同步代码块方式解决线程安全问题:
public class WindowTest1 {
public static void main(String[] args) {
Window1 win = new Window1();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable {
private int ticket = 100;
//创建一个唯一对象,做为锁,锁可以使任何对象,但切记锁智能有一把
Object obj = new Object();
@Override
public void run() {
while (true) {
//这里也可以用this 替换obj,因为我们只new了一个Window
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
此时,运行结果中就不会有重票出现了
2.同步方法
如果操作共享数据的代码完整的声明在 一个方法中,我们不妨将此方法声明为同步的,在方法声明处加
synchronized
标识符。
如下代码演示使用同步方法来解决线程安全问题:
public class WindowTest3 {
public static void main(String[] args) {
Window3 win = new Window3();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable {
private int ticket = 100;
//创建一个唯一对象,做为锁,锁可以使任何对象,但切记锁智能有一把
@Override
public void run() {
while (true) {
this.show();
if (ticket <= 0) {
break;
}
}
}
private synchronized void show() {//同步监视器中,这块默认的锁就是this
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
}
}
}
此时,错票数据就不会出现了
注意: 因为此时只有一个
Window3
对象,所以同步监视器默认就是this
3.Lock 锁 ReentrantLock
JDK 5.0 中新增了可以使用Lock 锁方式解决线程安全问题。
如下代码演示如何通过Lock锁方式,手动加锁和解锁
程序在run方法中,先手动加锁,保证只有当前线程拿到锁后其他线程不会进来,当执行到finally时候,释放锁,这时其他线程才能进来。
package com.jerry.thread6;
import java.util.concurrent.locks.ReentrantLock;
public class WindowTest5 {
public static void main(String[] args) {
Window5 win = new Window5();
Thread t1 = new Thread(win);
Thread t2 = new Thread(win);
Thread t3 = new Thread(win);
t1.start();
t2.start();
t3.start();
}
}
class Window5 implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//手动加锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
//手动解锁
lock.unlock();
}
}
}
}
总结:
面试题一:synchronized 和 lock方式,有什么不同?
相同点: 二者都可以解决线程安全问题。
不同点: lock 方式,是手动加锁和解锁。synchronized
是自动解锁的,他是在执行完同步方法或同步代码块才会自动释放同步监视器(自动解锁)。手动加锁,解锁,更加灵活
面试题二:如何解决线程安全问题?
1)加同步代码块
synchronized
关键字
2)使用同步方法 ,即加
synchronized
关键字的方法
3)使用
ReentrantLock
手动加锁,手动解锁
单例中的线程安全
如下:
懒汉式代码,存在线程安全问题,因为当多线程创建单例时,可能会重复创建,解决懒汉式线程安全,一般有两种方法
2. 性能较低的懒汉式
这段代码,当第一次创建实例后,其他线程还要去判断锁,性能是比较低的。
class Bank1 {
private Bank1() {
}
private static Bank1 instance = null;
//这种写法,效率较低,因为第一次已经将instance创建出来了,其他线程再去用的时候,还得去再判断一下锁,效率比较低
public static Bank1 getInstance() {
synchronized (Bank1.class) {
if (instance == null) {
return new Bank1();
}
return instance;
}
}
}
1. 性能较高的懒汉式
双重判断,当第一个线程创建了实例对象之后,第二个线程看到他不是NULL 了,就不用去再次判断锁了
class Bank2 {
private Bank2() {
}
private static Bank2 instance = null;
//这种写法,做了双重判断,当第一个线程创建了实例对象之后,第二个线程看到他不是NULL 了,就不用去再次判断锁了
public static Bank2 getInstance() {
if (instance == null) {
synchronized (Bank2.class) {
if (instance == null) {
return new Bank2();
}
}
}
return instance;
}
}
三、 线程的死锁问题
什么是死锁?
- 1.不同线程分别占用对方的需要的同步资源不放弃,都在等待对方放弃自己所需要的同步资源,就形成了线程的死锁
- 2.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
打个比方:
两个人分别有两双筷子,但是拿的都是对方的筷子,都在等对方把筷子给自己才能去吃饭,这时两个人就僵住了,谁也吃不到饭,这个问题就类似死锁问题。
如下代码演示死锁
如下代码,使用了双重锁,第一个线程,先抓s1,再抓s2,第二个线程,先抓s2,再抓s1。
当线程1执行到sleep
时候,挂起了一会,此时线程2刚好运行后抓住s2 的锁不放,因为s2这块也有sleep
,这时线程1要去拿s2就拿不到,此时即出现了死锁。
public class DeadLockTest {
//死锁问题
//产生死锁的原因,sleep之后,死锁概率加大,两个锁互相等待对方释放锁,而又拿着对方的锁不放,就导致死锁
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
//使用继承方式创建线程
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
sleepTime(100);
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//使用实现Runnable 接口的方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
sleepTime(100);
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
private static void sleepTime(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
怎样避免死锁?
- 1.专门的算法,原则避免。
- 2.尽量减少同步资源的定义
- 3.尽量避免同步嵌套