线程安全问题
Posted jiatcode
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全问题相关的知识,希望对你有一定的参考价值。
线程安全
1.概念
多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行结果是一样的,其他变量的值和预期的一样,就称之为线程安全的,反之则是不安全的
2.问题演示
如下模拟一个抢票系统:
-
定义一个Ticket线程类
public class Ticket implements Runnable{ private int Count = 100;//100张票在售 public void run() { while (true){ //有剩余票数 if (Count>=0){ //睡眠100毫秒模拟网络延迟 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //输出模拟抢票结果 String name = Thread.currentThread().getName(); System.out.println(name+"执行;剩余票数:"+ Count); Count--; } } }
-
主程序里我们同时让三个线程调用同一个Ticket对象来进行抢票
public class TicketSafe { public static void main(String[] args) { Ticket ticket = new Ticket(); Thread thread = new Thread(ticket,"窗口一"); Thread thread2 = new Thread(ticket,"窗口二"); Thread thread3 = new Thread(ticket,"窗口三"); thread.start(); thread2.start(); thread3.start(); } }
-
预期结果应该是三个窗口各自抢票,剩余票数要实时的进行减少,而以上代码的实际效果如下:
窗口二执行;剩余票数:100 窗口一执行;剩余票数:100 窗口三执行;剩余票数:100 窗口一执行;剩余票数:97 窗口二执行;剩余票数:97 窗口三执行;剩余票数:97 窗口二执行;剩余票数:94 窗口一执行;剩余票数:94 窗口三执行;剩余票数:94 窗口二执行;剩余票数:91 窗口三执行;剩余票数:90 窗口一执行;剩余票数:90 窗口一执行;剩余票数:88 窗口二执行;剩余票数:88 ................
可以看出,结果和我们预期的完全不同,分析可知,当多个线程一起对执行一个Runnable接口对象时,会出现以上情况,多个线程结果相同,不符合逻辑,致错原因如下所示:
三个线程同时进入if方法,并发情况下先后打印了结果,其他线程还没来得及count--就打印了,所以下次三个线程打印了一样的结果,count--执行了三次,下次循环还是一样的问题,所以出现了以上结果
-
总结问题:
- 多个线程在操作共享的数据
- 操作共享数据的线程代码有多条
- 多个线程对共享数据有写操作
3.实现线程安全
1.思路
- 只要在某个线程修改共享数据时,阻止其他要修改该共享数据的线程进行,等待修改结束完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证数据的同步性
2.7种线程同步机制
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(Lock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic*)
3.同步代码块(synchronized)
-
定义一个锁对象,作为进入代码块的钥匙
-
将需要保证线程安全的代码加入到synchronized下的代码块中,起到安全作用
//创建锁对象:相当于打开代码块的钥匙 private Object obj = new Object(); public void run() { while (true){ //有剩余票数 //同步代码块,线程到这里的时候,都会去请求一个obj资源,只有一个线程可以拿到 //拿到obj的线程才能进入代码块,其他请求的线程只能继续等待,等待obj锁对象被释放 synchronized (obj){ ..................... } } }
-
只有获取到obj的线程才能执行代码块,其余的线程必须等该线程运行完释放锁,再获取资源
4.同步方法
-
同步方法与同步代码块类似,使用的是synchronized关键字,不过他是基于方法层面的,关键字在方法上
-
synchronized加在线程要运行的方法上,java会自动给该方法加上一个锁对象,类似同步代码块中的obj
-
只有线程拥有该锁对象时,才能运行方法,没有锁对象的方法需要在方法外等待
//对于非static方法,调用该方法的Runnable实现类对象实例就是锁对象即this,注意对于多个线程来说,他们的this得是同一个实例对象,不然达不到互斥作用,相当于synchronized(new Ticket()) //对于static方法,当前方法所在类的字节码对象就是锁对象,相当于synchronized(Ticket.class) private synchronized void threadSafe(){ if (Count>=0){ //睡眠100毫秒模拟网络延迟 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //输出模拟抢票结果 String name = Thread.currentThread().getName(); System.out.println(name+"执行;剩余票数:"+ Count); Count--; } } public void run() { while (true) { threadSafe(); } }
5.同步锁(Lock)
-
java.util.Concurrent.locks.Lock 机制提供了比synchronized关键字更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,lock有更强大的功能,更体现面向对象
-
同步锁的方法
public void lock(); //加同步锁 public void unlock(); //释放同步锁
-
锁有多种,后面会有详细专题,这里先暂时使用以下重入锁(释放后还可以被调用的锁),以下为使用方式
- 先创建Lock对象
- 将需要加锁的代码块放在try中
- 在try前加上锁,在finally中释放锁,以确保不会导致死锁
//创建一个lock对象,重入锁实例 //参数fair: // true---公平锁:所有线程都能公平的得到机会 // false(默认)---独占锁:只有第一个得到的线程可以使用,除非它主动放弃或者释放 Lock lock = new ReentrantLock(false); public void run() { while (true) { lock.lock();//加上锁,只要有这个方法就一定要在某处有unlock,否则会导致死锁 //为保证unlock一定被执行,使用try finally来实现 try{ if (Count>=0){ //睡眠100毫秒模拟网络延迟 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //输出模拟抢票结果 String name = Thread.currentThread().getName(); System.out.println(name+"执行;剩余票数:"+ Count); Count--; } }finally { //保证释放锁 lock.unlock(); } } }
实验结果发现:当重入锁ReentrantLock的参数为true,即公平锁时,每个线程是公平的获得执行方法的权力,结果是非常有规律的1,2,3,1,2,3;而当定义为独占锁时,才有随机的效果,每个线程谁先获得锁,就可以执行。
关于锁的小结
- synchronized是java内置的关键字,在jvm层面;Lock是java类,在编码层面
- synchronized无法获取锁的状态,Lock可以判断是否获取到锁
- synchronized可以主动释放锁,Lock需要手动unlock
- synchronized阻塞的线程获取不到锁就会一直等待一直阻塞,Lock阻塞的线程则不会,如果尝试获取不到锁,线程可以不用一直等待就结束了
- synchronized锁可重入,不可判断,非公平,而Lock都可以自己定义
- synchronized适合少量代码的同步问题,Lock适合大量同步的代码问题
以上是关于线程安全问题的主要内容,如果未能解决你的问题,请参考以下文章
HashMap 和 ConcurrentHashMap 的区别
newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段