前言
前几篇文章讲解了线程的生命周期,以及会进入不同的阶段与每个阶段做了什么事情
那么接下来我们要介绍的是线程安全问题,那么什么是线程安全问题?
什么是线程安全问题
我们这里举例的是多个线程同时运行,并同一个实现了Runnable的接口的类
程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。反之则是线程不安全的
问题演示
为了演示线程安全问题,我们采用多线程模拟多个窗口同时售卖影票
public class Ticket implements Runnable {
//假设售票为100张
private int ticketMum = 100;
//模拟售票过程,卖一张减一张
public void run() {
while (true) {
//判断是否有票,ticketNum>0
if (ticketMum > 0) {
//有票,让线程睡眠100毫秒
try {
//模拟售票间隔
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前售出的票数字和线程名,票数减一
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "销售电影票:" + ticketMum--);
}
}
}
}
此时我们先模拟一个窗口售票看看是什么情况?
public class TicketSaleMain {
public static void main(String[] args){
//1.创建电影票对象
Ticket ticket = new Ticket();
//2.创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1");
thread.start();
}
}
//运行结果如下:
线程窗口1销售电影票:100
线程窗口1销售电影票:99
线程窗口1销售电影票:98
......
线程窗口1销售电影票:2
线程窗口1销售电影票:1
接下来我们演示多线程的情况下售票,我们再new多几个窗口看看
public class TicketSaleMain {
public static void main(String[] args){
//1.创建电影票对象
Ticket ticket = new Ticket();
//2.创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1");
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
thread.start();
thread2.start();
thread3.start();
}
}
//运行结果如下:
线程窗口2销售电影票:100
线程窗口1销售电影票:100
线程窗口3销售电影票:99
......
线程窗口1销售电影票:2
线程窗口2销售电影票:1
线程窗口3销售电影票:0
当多个线程售卖的时候,就会发现100售卖了两次,以及还出现了售票:0的情况
问题分析
线程安全问题都是由全局变量及静态变量引起的
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全
综上所述,线程安全问题根本原因:
- 多个线程在操作共享的数据
- 操作共享数据的线程代码有多条
- 多个线程对共享数据有写操作
要解决以上线程问题,我们的思路是只要在某个线程修改共享资源的时候
使其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPu资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(BeeneantLock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic*)
采用同步代码块解决问题
我们以刚刚的示例类举例示范一下,每个窗口售票都采用while里面的代码进行售票
那么我们针对这段代码进行同步代码块的限制,只有一个线程能够进入这段代码,这个线程执行完毕后,另一个线程才能访问这段代码。这是不是就保证线程安全
我们的思路是:创建锁对象/采用类的锁,当做钥匙
class Ticket implements Runnable {
//假设售票为100张
private int ticketMum = 100;
//锁对象
private Object obj = new Object();
//模拟售票过程,卖一张减一张
public void run() {
while (true) {
synchronized (obj){
//判断是否有票,ticketNum>0
if (ticketMum > 0) {
//有票,让线程睡眠100毫秒
try {
//模拟售票间隔
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前售出的票数字和线程名,票数减一
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "销售电影票:" + ticketMum--);
}
}
}
}
}
这时我们再采用多个窗口进行售票看看,是否还会有100出现两次以及出现0的情况
public c1ass TicketSaleMain {
public static void main(String[] args){
//1.创建电影票对象
Ticket ticket = new Ticket();
//2.创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1"):
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
thread.start() :
}
}
线程窗口2销售电影票:100
线程窗口3销售电影票:99
线程窗口1销售电影票:98
......
线程窗口3销售电影票:3
线程窗口3销售电影票:2
线程窗口3销售电影票:1
采用同步方法解决问题
接下来我们采用同步方法的思路解决问题,那么我们就需要一个方法
private synchronized void saleTicke(){
}
我们采用synchronized加载到方法上和加载到代码块类似,但是并没有显示出来,锁对象在哪呢?
如果synchronized加载的方法不是静态方法
,而是实例方法的话,那么锁对象就是当前调用方法的this
//等价于
private synchronized(new Ticket()) void saleTicke(){
}
如果`synchronized加载的方法是静态方法,那么就是使用该方法所在的类,类.class作为锁
//等价于
private static synchronized(Ticket.class) void saleTicke(){
}
此时我们就可以把我们需要同步的代码,放到我们创建的方法当中去
private synchronized void saleTicke(){
//判断是否有票,ticketNum>0
if (ticketMum > 0){
//有票,让线程睡眠100毫秒
try{
//模拟售票间隔
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
//打印当前售出的票数字和线程名,票数减一
String name = Thread.currentThread().getName ();
System.out.println("线程"+name+"销售电影票:"+ticketMum--);
}
}
//模拟售票过程,卖一张减一张
public void run() {
while (true) {
saleTicke();
}
}
这时我们再采用多个窗口进行售票看看,是否还会有100出现两次以及出现0的情况
public c1ass TicketSaleMain {
public static void main(String[] args){
//1.创建电影票对象
Ticket ticket = new Ticket();
//2.创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1"):
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
thread.start() :
}
}
线程窗口1销售电影票:100
线程窗口1销售电影票:99
线程窗口2销售电影票:98
......
线程窗口2销售电影票:3
线程窗口3销售电影票:2
线程窗口3销售电影票:1
采用同步锁解决问题
接下来要采用的是java.util.concurrent.locks.Lock机制
上面两个方法采用的是synchronized代码块和 synchronized方法
相比这两方法,我们这Lock锁,同步代码块/同步方法具有的功能Lock都有,更强大,更体现面向对象
主要涉及的方法如下:
public void lock()//加锁
public void unlock()//释放锁
class Ticket implements Runnable {
//假设售票为100张
private int ticketMum = 100;
//Lock锁对象
//ReentrantLock 重入锁 用锁的时候,再请求锁依然可拿
//true 公平锁,多个线程都有公平执行权
//false 非公平锁,即一个线程占有后除非释放,其他不可
private Lock lock = new ReentrantLock(true);
//模拟售票过程,卖一张减一张
public void run() {
while (true) {
lock.lock();//加锁
//判断是否有票,ticketNum>0
try {
if (ticketMum > 0){
//有票,让线程睡眠100毫秒
try{
//模拟售票间隔
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
//打印当前售出的票数字和线程名,票数减一
String name = Thread.currentThread().getName ();
System.out.println("线程"+name+"销售电影票:"+ticketMum--);
}
} finally {
lock.unlock();//解锁
}
}
}
}
需要注意,我们使用lock的话,需要保证lock被调用后,unlock也要调用,否则会出现死锁
可通过try,finally的方法保证释放锁,接下来让我们测试测试是否可以保证线程安全
public c1ass TicketSaleMain {
public static void main(String[] args){
//1.创建电影票对象
Ticket ticket = new Ticket();
//2.创建Thread对象,执行电影票售卖
Thread thread = new Thread(ticket,"窗口1"):
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
thread.start() :
}
}
线程窗口1销售电影票:100
线程窗口3销售电影票:99
线程窗口2销售电影票:98
......
线程窗口3销售电影票:3
线程窗口2销售电影票:2
线程窗口1销售电影票:1
Synchronized和Lock区别
synchronized是 java内置关键字,在jivm层面,Lock是个 java类
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
synchronized会自动释放锁(a线程执行完同步代码会释放锁﹔b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。