前言对于从事后端开发的同学来说,线程安全是我们每天都需要考虑的问题。一般来说,线程安全问题主要是在多线程环境下,不同线程同时读写公共资源(关键资源)导致的数据异常。例如:变量a=0,线程1给这个变量+1,线程2也给这个变量+1。这时候线程3获取到的a的值可能不是2,而是1,线程3不会获取错数据吧?线程安全问题会直接导致数据异常,进而影响业务功能的正常使用,所以这个问题还是很严重的。那么,如何解决线程安全问题呢?今天和大家聊聊,保证线程安全的11个技巧,希望对大家有所帮助。1.无状态我们都知道,只有当多个线程访问公共资源时,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题了?例如:publicclassNoStatusService{publicvoidadd(Stringstatus){System.out.println("addstatus:"+status);}publicvoidupdate(Stringstatus){System.out.println("更新状态:"+status);}}在这个例子中,NoStatusService没有定义公共资源,换句话说它是无状态的。在这种情况下,NoStatusService类必须是线程安全的。2.不可变如果多线程访问的公共资源是不可变的,就不会有数据安全问题。例如:publicclassNoChangeService{publicstaticfinalStringDEFAULT_NAME="abc";publicvoidadd(Stringstatus){System.out.println(DEFAULT_NAME);}}DEFAULT_NAME被定义为一个staticfinal常量,在多线程环境下不会被修改,所以在这种情况下,不会有线程安全问题。3、没有修改权限有时候,我们定义了一个公共资源,但是这个资源只暴露了读取的权限,没有暴露修改的权限,也是线程安全的。例如:publicclassSafePublishService{privateStringname;publicStringgetName(){返回名称;}publicvoidadd(Stringstatus){System.out.println("添加状态:"+status);}}在这个例子中,没有修改name字段的入口对外暴露,所以不存在线程安全问题。4.Synchronized使用了JDK提供的同步机制,使用方式也有很多,分为:同步方法和同步代码块。我们优先使用synchronized代码块,因为synchronized方法的粒度是整个方法,作用范围太大,相对来说对代码性能消耗更大。实际上,每个对象内部都有一把锁,只有抢到锁的线程才能进入相应的代码块,执行相应的代码。当代码块执行时,底层JVM会自动释放锁。例如:publicclassSyncService{privateintage=1;私有对象对象=新对象();//同步方法publicsynchronizedvoidadd(inti){age=age+i;System.out.println("年龄:"+年龄);}publicvoidupdate(inti){//同步代码块,对象锁synchronized(object){age=age+i;System.out.println("年龄:"+年龄);}}publicvoidupdate(inti){//同步代码块,类锁synchronized(SyncService.class){age=age+i;System.out.println("年龄:"+年龄);}}}五、Lock使用synchronized除了通过关键字实现同步功能,JDK还提供了Lock接口,这是一种显示锁的方式。通常我们会用到Lock接口的实现类:ReentrantLock,它包括:公平锁、非公平锁、重入锁、读写锁等更强大的功能。例如:publicclassLockService{privateReentrantLockreentrantLock=newReentrantLock();公共年龄=1;publicvoidadd(inti){try{reentrantLock.lock();年龄=年龄+我;System.out.println("年龄:"+age);}最后{reentrantLock.unlock();但是如果使用ReentrantLock,也会带来一个小问题:需要在finally代码块中手动释放锁。不过说实话,使用Lock来显示锁解决了线程安全问题,也为开发者提供了更大的灵活性。6.如果分布式锁是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。但是如果在分布式环境下,即一个应用部署了多个节点,每个节点都可以使用synchronized和Lock来保证线程安全,但是不同节点之间不能保证线程安全。这就需要使用:分布式锁。分布式锁有很多种,比如:数据库分布式锁、zookeeper分布式锁、redis分布式锁等,其中我个人比较推荐使用redis分布式锁,相对效率更高一些。使用redis分布式锁的伪代码如下:try{Stringresult=jedis.set(lockKey,requestId,"NX","PX",expireTime);如果(“确定”。等于(结果)){返回真;}returnfalse;}finally{unlock(lockKey);}也需要在finally代码块中释放锁。七、volatile有时候,我们有这样一个需求:如果多个线程中有一个线程将某个switch的状态设置为false,则整个函数停止。经过简单的需求分析,发现只需要多线程之间的可见性,不需要原子性。如果一个线程修改了状态,所有其他线程都可以获得最新的状态值。这样的分析很容易搞定,使用volatile可以很快满足需求。例如:@ServicepublicCanalService{privatevolatilebooleanrunning=false;私有线程线程;@Autowired私有CanalConnectorcanalConnector;publicvoidhandle(){//connectcanalwhile(running){//业务处理}}publicvoidstart(){thread=newThread(this::handle,"name");运行=真;thread.start();}publicvoidstop(){if(!running){返回;}运行=假;}}需要特别注意的地方是:volatile不能用在计数、统计等业务场景中。因为volatile不能保证操作的原子性,可能会出现数据异常。8.ThreadLocal除了上述解决方案,JDK还提供了另一种用空间换取时间的新思路:ThreadLocal。当然ThreadLocal并不能完全替代锁,尤其是在一些秒杀更新库存的时候,必须要用到锁。ThreadLocal的核心思想是:共享变量在每个线程中都有一个副本,每个线程操作自己的副本,对其他线程没有影响。温馨提示:我们平时使用ThreadLocal的时候,在使用完之后,一定要记得在finally代码块中调用它的remove方法来清除数据,否则可能会出现内存泄漏。例如:publicclassThreadLocalService{privateThreadLocal
