线程安全

320 阅读2分钟

线程安全指的是多线程共享变量,导致访问数据出问题。

例:超卖问题

/**
* 模拟3个窗口卖100张票
*
*/
public class TestThread{
    static int ticket = 100;
    public static void main(String[] args){
        Runnable runnable =()->{
            while(true){
                try{
                    Thread.sleep(1);
                }catch(InteruptedException e){
                    e.printStackTrace();
                }
                if(ticket>0){
                    System.out.print(Thread.currentThread.getName()+"卖了第"+ticket--+"张票");
                }else{
                    break;
                }
            }
        }
        
        Thread t1=new Thread(runnable,"窗口1");
        Thread t2=new Thread(runnable,"窗口2");
        Thread t3=new Thread(runnable,"窗口3");
        
        t1.start();
        t2.start();
        t3.start();
        
    }
}

运行结果:

可以看出窗口2已经卖出了第100张票,而窗口1又把第100张票卖出了,这里明显是存在bug的。

原因分析

java内存模型(Java Memory Model,简称JMM)

主线程的共享变量是所有线程共享的,而工作内存是线程私有的。当一个线程要修改共享变量时,首先从主内存中将共享变量复制到工作内存中,修改完成后再写回主内存。这就可能出现一个问题,就是当修改完成了 但还未写会主内存中,其他线程也开始对共享变量进行操作,此时读取的数据是主内存中的数据,产生了数据不一致问题。

并发问题的三大特性

  1. 原子性

    一个操作或多个操作 ,要么全部执行且在执行的过程中不被打断,要么全部不执行。

  2. 可见性

    当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  3. 有序性

    JVM为了提高运行效率,会对代码进行优化,不保证程序中语句的先后顺序,但会保证程序最终运行结果,这就是指令重排序(happen-before)。指令重排序对单线程没有影响,对多线程就会产生影响。

解决办法1:volatile关键字

  1. 保证变量的可见性
  2. 屏蔽指令重排序

解决办法2:synchronized

保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的方法是可见的,可以代替volatile

底层原理:

当线程A要执行被synchronized加锁的代码块时,首先通过monitorenter来获得锁,此时若线程B也执行到了这个代码块,需要等待,当线程A运行结束后,通过monitorexit来释放锁,此时线程B才能获得这个代码块的锁。

解决办法3:lock锁

Lock接口提供了与synchronized关键字类似的功能,但需要手动获取锁与释放锁。

代码形式:

Lock lock=new ReentrantLock();
lock.lock();
try{
    //需要加锁的代码块
}finally{
    lock.unlock();
}

lock锁相比于synchronized提供了更加丰富的Api,例如trylock(),尝试获取锁,若获取失败可以执行其他操作,不会等待。