摘要:多线程(并发)场景下,如何编写线程安全(Thread-Safety)的程序,对于程序的正确和稳定运行有重要的意义。下面将结合示例,谈谈如何在Java语言中,实现线程安全的程序。
本文分享自华为云社区《Java如何实现多线程场景下的线程安全》,做者: jackwangcumt 。java
当前随着计算机硬件的快速发展,我的电脑上的CPU也是多核的,如今广泛的CUP核数都是4核或者8核的。所以,在编写程序时,须要为了提升效率,充分发挥硬件的能力,则须要编写并行的程序。Java语言做为互联网应用的主要语言,普遍应用于企业应用程序的开发中,它也是支持多线程(Multithreading)的,但多线程虽好,却对程序的编写有较高的要求。安全
单线程能够正确运行的程序不表明在多线程场景下可以正确运行,这里的正确性每每不容易被发现,它会在并发数达到必定量的时候才可能出现。这也是在测试环节不容易重现的缘由。所以,多线程(并发)场景下,如何编写线程安全(Thread-Safety)的程序,对于程序的正确和稳定运行有重要的意义。下面将结合示例,谈谈如何在Java语言中,实现线程安全的程序。多线程
为了给出感性的认识,下面给出一个线程不安全的示例,具体以下:并发
package com.example.learn; public class Counter { private static int counter = 0; public static int getCount(){ return counter; } public static void add(){ counter = counter + 1; } }
这个类有一个静态的属性counter,用于计数。其中能够经过静态方法add()对counter进行加1操做,也能够经过getCount()方法获取到当前的计数counter值。若是是单线程状况下,这个程序是没有问题的,好比循环10次,那么最后获取的计数counter值为10。但多线程状况下,那么这个结果就不必定可以正确获取,可能等于10,也可能小于10,好比9。下面给出一个多线程测试的示例:less
package com.example.learn; public class MyThread extends Thread{ private String name ; public MyThread(String name){ this.name = name ; } public void run(){ Counter.add(); System.out.println("Thead["+this.name+"] Count is "+ Counter.getCount()); } } /////////////////////////////////////////////////////////// package com.example.learn; public class Test01 { public static void main(String[] args) { for(int i=0;i<5000;i++){ MyThread mt1 = new MyThread("TCount"+i); mt1.start(); } } }
这里为了重现计数的问题,线程数调至比较大,这里是5000。运行此示例,则输出可能结果以下:性能
Thead[TCount5] Count is 4 Thead[TCount2] Count is 9 Thead[TCount4] Count is 4 Thead[TCount14] Count is 10 .................................. Thead[TCount4911] Count is 4997 Thead[TCount4835] Count is 4998 Thead[TCount4962] Count is 4999
注意:多线程场景下,线程不安全的程序输出结果具备不肯定性。测试
基于上述的示例,让其变成线程安全的程序,最直接的就是在对应的方法上添加synchronized关键字,让其成为同步的方法。它能够修饰一个类,一个方法和一个代码块。对上述计数程序进行修改,代码以下:this
package com.example.learn; public class Counter { private static int counter = 0; public static int getCount(){ return counter; } public static synchronized void add(){ counter = counter + 1; } }
再次运行程序,则输出结果以下:atom
...... Thead[TCount1953] Count is 4998 Thead[TCount3087] Count is 4999 Thead[TCount2425] Count is 5000
另一种常见的同步方法就是加锁,好比Java中有一种重入锁ReentrantLock,它是一种递归无阻塞的同步机制,相对于synchronized来讲,它能够提供更增强大和灵活的锁机制,同时能够减小死锁发生的几率。示例代码以下:spa
package com.example.learn; import java.util.concurrent.locks.ReentrantLock; public class Counter { private static int counter = 0; private static final ReentrantLock lock = new ReentrantLock(true); public static int getCount(){ return counter; } public static void add(){ lock.lock(); try { counter = counter + 1; } finally { lock.unlock(); } } }
再次运行程序,则输出结果以下:
...... Thead[TCount1953] Count is 4998 Thead[TCount3087] Count is 4999 Thead[TCount2425] Count is 5000
注意:Java中还提供了读写锁ReentrantReadWriteLock,这样能够进行读写分离,效率更高。
因为锁机制会影响必定的性能,而有些场景下,能够经过无锁方式进行实现。Java内置了Atomic相关原子操做类,好比AtomicInteger, AtomicLong, AtomicBoolean和AtomicReference,能够根据不一样的场景进行选择。下面给出示例代码:
package com.example.learn; import java.util.concurrent.atomic.AtomicInteger; public class Counter { private static final AtomicInteger counter = new AtomicInteger(); public static int getCount(){ return counter.get(); } public static void add(){ counter.incrementAndGet(); } }
再次运行程序,则输出结果以下:
...... Thead[TCount1953] Count is 4998 Thead[TCount3087] Count is 4999 Thead[TCount2425] Count is 5000
前面提到,线程不安全的一个缘由就是多个线程同时访问某个对象中的数据,数据存在共享的状况,所以,若是将数据变成独享的,即无状态(stateless)的话,那么天然就是线程安全的。而所谓的无状态的方法,就是给一样的输入,就能返回一致的结果。下面给出示例代码:
package com.example.learn; public class Counter { public static int sum (int n) { int ret = 0; for (int i = 1; i <= n; i++) { ret += i; } return ret; } }
前面提到,若是须要在多线程中共享一个数据,而这个数据给定值,就不能改变,那么也是线程安全的,至关于只读的属性。在Java中能够经过final关键字进行属性修饰。下面给出示例代码:
package com.example.learn; public class Counter { public final int count ; public Counter (int n) { count = n; } }
前面提到了几种线程安全的方法,整体的思想要不就是经过锁机制实现同步,要不就是防止数据共享,防止在多个线程中对数据进行读写操做。另外,有些文章中说到,能够在变量前使用volatile修饰,来实现同步机制,但这个通过测试是不必定的,有些场景下,volatile依旧不能保证线程安全。虽然上述是线程安全的经验总结,可是仍是须要经过严格的测试进行验证,实践是检验真理的惟一标准。