一、线程安全概述
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。但在多线程的情况中往往会出现线程不安全的问题。我们通过一个电影院卖票的案例来模拟线程安全的情况。
案例描述:电影院卖100张票,三个售票口同时卖。
先定义一个类来模拟票:
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
// 重写run方法,设置线程任务:卖票
@Override
public void run() {
// 每个窗口卖票的操作,窗口一直开启。使用循环,让卖票操作重复执行
while (true) {
// 先判断票是否存在
if (ticket > 0) {
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--; }
}
}
}
再定义测试类:
public class Demo01Ticket {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
Runnable run = new RunnableImpl();
// 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run,"1号窗口");
Thread t1 = new Thread(run,"2号窗口");
Thread t2 = new Thread(run,"3号窗口");
// 调用start方法开启多线程,开始卖票
t0.start();
t1.start();
t2.start();
}
}
部分运行结果如下图:
从该运行结果中可以看出两个问题:
- 第4张票被卖了三次,每个窗口一次
- 出现了不存在的票——第0张票
上面的这种问题就成为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
二、线程同步
1.概述
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
Java中解决多线程并发访问一个资源的安全性问题的办法是:同步机制(synchronized)。
以售票案例为例:窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
2.实现同步机制的三种方法
- 1) 同步代码块
- 2) 同步方法
- 3) 锁机制
三、同步代码块
1.格式
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
2.使用
对模拟票的类进行改造:
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
// 创建一个锁对象 !*****!
Object obj = new Object();
// 设置线程任务:卖票
@Override
public void run() {
// 使用循环,让卖票操作重复执行
while (true) {
// 创建一个同步代码块 !*****!
synchronized (obj) {
// 先判断票是否存在
if (ticket > 0) {
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
测试类不变。使用了同步代码块后,售票案例中的线程安全问题解决了。
四、同步方法
1.格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
2.使用步骤
- 1)把访问了共享数据的代码块抽取出来,放到一个方法中
- 2)在方法上添加synchronized修饰符
3.使用
对模拟票的类进行改造:
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
// 创建一个锁对象
Object obj = new Object();
// 设置线程任务:卖票
@Override
public void run() {
// 使用循环,让卖票操作重复执行
while (true) {
sellTicket();
}
}
/*
定义一个同步方法
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象是谁?
就是实现类对象new RunnableImpl()
*/
public synchronized void sellTicket(){
// 先判断票是否存在
if (ticket > 0) {
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
测试类不变。使用了同步方法后,售票案例中的线程安全问题解决了。
五、锁机制
1.概述
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
2.使用步骤
- 1)在成员位置创建一个ReentrantLock对象
- 2)在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
- 3)在可能会出现安全问题的代码后调用Lock接口中的方法unlock获取锁
java.util.concurrent.locks.ReentrantLock implements Lock
ReentrantLock一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
3.使用
对模拟票的类进行改造:
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
// 1. 在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
// 设置线程任务:卖票
@Override
public void run() {
// 使用循环,让卖票操作重复执行
while (true) {
// 2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
l.lock();
// 先判断票是否存在
if (ticket > 0) {
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
// 票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock获取锁
l.unlock(); // 无论程序是否异常,都会把锁释放
}
}
}
}
}
测试类不变。使用了锁机制后,售票案例中的线程安全问题解决了。
六、线程的生命周期
生命周期是指组件从创建到销亡的过程中,经历不同的阶段和状态
线程对象的生命周期一共有六种状态,当对线程对象调用不同的方法时,会引起线程状态间的转换
1. 新建状态—New
使用new关键字创建线程对象时的状态(已创建未启动)
2. 可运行状态—Runnable
调用start()方法启动线程后的状态,该状态对应操作系统中的就绪和运行两种状态
(1)就绪状态—start()方法内部调用本地方法start0(),操作系统会为该线程分配除CPU以外的所有资源
(2)运行状态—处于就绪状态的线程被线程调度器选中获取到了CPU执行权,正在执行run()方法
3. 阻塞状态—Blocked
线程由于未持有对象的锁定,而被阻挡在synchronized同步代码块外或同步方法外时的状态
一旦线程获取了对象的锁定,就会自动变为可运行状态
线程同步时,只有获取了锁的线程,才能进入同步代码块或同步方法中,操作共享资源数据
4. 等待状态—Waiting
线程执行了无参的join()方法或wait()方法后,会进入等待状态
直到所连接的线程执行结束或其它线程执行notify()或notifyAll()方法,线程会自动变为可运行状态
5. 计时等待状态—Timed_Waiting
线程执行了sleep()方法,有参的join()方法或wait()方法后,会进入计时等待状态
直到休眠时间结束,或所连接的线程执行结束,或其它线程执行notify()|notifyAll()方法,或达到了超时时间,线程会自动变为可运行状态
6. 终止状态—Terminated
线程run()方法中的代码正常执行完毕,或执行过程中抛出未捕获的异常退出后的状态