线程安全
再写一个关于线程安全的,很多人都喜欢讨论多线程怎么使用,什么AQS、CAS、对象监视。但是如果线程安全的基本定义没有完全搞清楚的话,多线程用起来还是有点儿可怕的。
什么是线程安全
官方一点儿的说法,多个线程要同时修改一个变量时,要保证一个变量的原子性、可见性、有序性。其实说白了就是,多个线程修改,你要保证每个线程的修改都是对的(等于没说,好像)。
首先,为什么会有线程不安全呢?这是由jvm自身的特性决定的,jvm中有主内存和线程本地内存(线程栈)之说,每一个线程在修改变量的值的时候,都得先去主内存中将该变量拷贝到线程栈里面修改,然后将修改后的值再同步到主内存中。
举个例子:比如说我们现在在卖周杰伦演唱会的门票,门票剩余五万张,我们采用多线程去做卖票这个事儿,票的剩余数量ticketsCount现在剩余50000,我们开了35个线程同时卖票,这时候可能35个线程都要去修改这个剩余票数,怎么修改的?
1、从主内存中读取剩余票数,50000
2、在线程栈中修改票数,做减一操作,线程栈中修改后的ticketsCount的值49999
3、将修改后的票数49999刷回主内存
这就出问题了,其实我们卖了多少呢?35张,可是剩余票数还有49999张,出问题了,超卖!这就是典型的线程不安全问题。
那怎么解决呢?有很多种方法
1、采用原子类,juc中给我们提供了很多基础类型的线程安全版本,如int 的线程安全类AtomicInteger、long的AtomicLong、AtomicBoolean等,在修改这种类型的变量时无需考虑原子性、可见性问题,但线程的修改顺序需要你自己去控制。
2、采用Lock,李叔在juc中也给我们提供了很多的线程安全的辅助类,ReentranLock、ReentranReadWriteLock、Semaphore等控制可见性、原子性、有序性的工具类,来帮助我们实现线程安全。这种的其实又牵扯到AQS,各种的BlockQueue、LeaderFlowller模式等,很多东西,Lock的有序性由线程存放的队列保证。
3、利用jvm自身的线程安全机制,如Synchronized,甚至是LockSupport中的park unpark方法,但是LockSupport还是慎用。Synchronized的其实就是java的对象监听器、mointorEnter、monitorExit等
还有一个比较容易引起误会的关键字volatile,这个关键字很有个性,而且不容易用好,首先这个关键字不能保证线程安全,上面已经说了线程安全最起码包括三方面,有序性、原子性、可见性,但是volatile只能保证可见性,原理是处理器的嗅探和lock内存屏障,当被volatile修饰的变量发生修改时,这个变量的值会被强制的同步到主内存中,同时各个处理器也会使用嗅探技术去主内存同步这变量,当进行修改时就会使用最新的变量值。
可是当他同步过来之后。我已经用老的值计算过了呢?岂不是我的数据是错的了?是的,确实是错了。这就是为什么volatile不能保证线程安全,因为没法保证原子性和有序性。所以volitale不能保证线程安全,关于内存屏障和总线嗅探,各位看官可以再查资料好,还是比较多的。那有人说,这个关键字不是个鸡肋吗?还有什么用?其实是有用的,怎么用呢?有一些业务场景是这样的,我这个变量修改的地方只有一个,但是我这个变量很多地方都会用啊,而且是多线程使用,但是绝不会修改,那你就可以放心大胆的用这个关键字了,效率比上面的三种还高。
所以,技术是积累的,厚积薄发。。。。。。。。。。一股老年风扑面而来。
现在都不是单节点部署了,那在集群状态下也会遇到线程安全问题,那怎么办呢?有人说用数据库的事务做并发控制,这种其实是比较奢侈的,因为都知道数据库是昂贵资源,这时候一般都是分布式锁,分布式锁有很多。
典型的就是redis和zookeeper的,这个写起来比较长,下一篇写。