当前位置: 首页 > 后端技术 > Java

5招教你在多线程场景下实现线程安全!

时间:2023-04-01 14:52:47 Java

摘要:在多线程(并发)场景下,如何编写线程安全(Thread-Safety)的程序,对于程序的正确稳定运行具有重要意义。下面将结合实例来谈谈如何用Java语言实现线程安全的程序。本文分享自华为云社区《Java如何实现多线程场景下的线程安全》,作者:jackwangcumt。1引言随着计算机硬件的飞速发展,个人电脑上的CPU也是多核的,现在常见的CPU内核为4核或8核。因此,在编写程序时,为了提高效率,充分发挥硬件的能力,就需要编写并行程序。Java语言作为互联网应用的主要语言,被广泛应用于企业应用的开发中。它还支持多线程(Multithreading),但是多线程虽然好,但是对程序编写的要求更高。一个程序在单线程下能正确运行,并不代表在多线程场景下也能正确运行。这里的正确性往往不容易发现,只有在并发数达到一定数量时才会出现。这也是测试环节不容易复现的原因。因此,在多线程(并发)场景下,如何编写线程安全(Thread-Safety)的程序,对于程序的正确稳定运行具有重要意义。下面将结合实例来谈谈如何用Java语言实现线程安全的程序。为了给人一个感性的理解,下面给出一个线程不安全的例子,如下:packagecom.example.learn;publicclassCounter{privatestaticintcounter=0;publicstaticintgetCount(){返回计数器;}publicstaticvoidadd(){counter=counter+1;}}这个类有一个静态属性counter,用于计数。计数器可以通过静态方法add()加1,也可以通过getCount()方法获取当前计数器值。如果是单线程的情况,这个程序是没有问题的。比如循环10次,那么最后得到的counter值就是10。但是在多线程的情况下,可能不能正确得到结果,可能等于10,也可能小于10,比如9.下面给出了一个多线程测试的例子:packagecom.example.learn;publicclassMyThreadextendsThread{privateStringname;publicMyThread(Stringname){this.name=name;}publicvoidrun(){Counter.add();System.out.println("Thead["+this.name+"]计数为"+Counter.getCount());}}////////////////////////////////////////////////////////packagecom.example.learn;publicclassTest01{publicstaticvoidmain(String[]args){for(inti=0;i<5000;i++){MyThreadmt1=newMyThread("TCount"+i);mt1.开始();}}}这里为了重现计数问题,将线程数调整到一个比较大的值,这里是5000。运行这个例子,输出可能是这样的:Thead[TCount5]Countis4Thead[TCount2]Count是9Thead[TCount4]计数是4Thead[TCount14]计数是10.........Thead[TCount4911]计数是4997Thead[TCount4835]计数是4998Thead[TCount4962]计数是4999注:在多线程场景,线程不安全的程序输出是不确定的。2synchronized方法是基于上面的例子,要让它成为一个线程安全的程序,最直接的方法就是在相应的方法中加上synchronized关键字,使其成为一个synchronized方法。它可以装饰一个类、一个方法和一个代码块。修改上面的计数程序,代码如下:packagecom.example.learn;公共类Counter{privatestaticintcounter=0;publicstaticintgetCount(){返回计数器;}publicstaticsynchronizedvoidadd(){counter=counter+1;}}再次运行程序,输出如下:......Thead[TCount1953]Countis4998Thead[TCount3087]Countis4999Thead[TCount2425]Countis50003另一种常见的同步方式锁机制是Locking,例如,Java中有一个可重入锁ReentrantLock,它是一种递归的非阻塞同步机制。与synchronized相比,它可以提供更强大、更灵活的锁机制,可以降低死锁的概率。示例代码如下:packagecom.example.learn;importjava.util.concurrent.locks.ReentrantLock;publicclassCounter{privatestaticintcounter=0;privatestaticfinalReentrantLocklock=newReentrantLock(true);publicstaticintgetCount(){返回计数器;}publicstaticvoidadd(){lock.lock();尝试{计数器=计数器+1;}最后{lock.unlock();}}}再次运行程序,输出如下:......Thead[TCount1953]Countis4998Thead[TCount3087]Countis4999Thead[TCount2425]Countis5000注意:Java也提供了读写锁ReentrantReadWriteLock,这样可以进行读写分离,效率更高。4使用Atomic对象由于锁机制会影响一定的性能,在某些场景下可以采用无锁的方式实现。Java内置了Atomic相关的原子操作类,如AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,可以根据不同的场景选用。示例代码如下:publicstaticintgetCount(){返回counter.get();}publicstaticvoidadd(){counter.incrementAndGet();}}再次运行程序,输出结果如下:......Thead[TCount1953]Countis4998Thead[TCount3087]Countis4999Thead[TCount2425]Countis50005Statelessobjects前面说了原因之一线程不安全就是多个线程同时访问一个对象中的数据,数据是共享的。所以,如果数据变成了独占,就是无状态的(stateless),那么自然是线程安全的。所谓无状态方法就是给同样的输入,返回一致的结果。示例代码如下:packagecom.example.learn;publicclassCounter{publicstaticintsum(intn){intret=0;for(inti=1;i<=n;i++){ret+=i;返回ret;}}6Immutableobjects前面说到,如果需要在多个线程中共享一段数据,并且数据在给定值后不能改变,它也是线程安全的,相当于只读属性。在Java中,可以使用final关键字修改属性。示例代码如下:packagecom.example.learn;publicclassCounter{publicfinalintcount;公共计数器(intn){count=n;}}7总结一下上面提到的几种线程安全的方法,总体思路要么是通过锁机制实现同步,要么就是防止数据共享,防止数据在多线程中被读写。另外,有的文章提到可以在变量前使用volatile修饰符来实现同步机制,但是经过测试并不确定。在某些场景下,volatile仍然不能保证线程安全。以上虽然是线程安全方面的经验总结,但仍然需要通过严格的测试来验证。实践是检验真理的唯一标准。点击关注,第一时间了解华为云的新鲜技术~