线程安全问题

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 的区别

线程同步-使用ReaderWriterLockSlim类

newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段

线程安全问题的概述和线程安全的代码实现与问题产生的原理

多线程 Thread 线程同步 synchronized

活动到片段方法调用带有进度条的线程