摘要: 多线程(并发)场景下,如何编写线程平安(Thread-Safety)的程序,对于程序的正确和稳固运行有重要的意义。上面将联合示例,谈谈如何在 Java 语言中,实现线程平安的程序。
本文分享自华为云社区《Java 如何实现多线程场景下的线程平安》,作者:jackwangcumt。
1 引言
以后随着计算机硬件的疾速倒退,个人电脑上的 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。上面给出一个多线程测试的示例:
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
留神:多线程场景下,线程不平安的程序输入后果具备不确定性。
2 synchronized 办法
基于上述的示例,让其变成线程平安的程序,最间接的就是在对应的办法上增加 synchronized 关键字,让其成为同步的办法。它能够润饰一个类,一个办法和一个代码块。对上述计数程序进行批改,代码如下:
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;}
}
再次运行程序,则输入后果如下:
......
Thead[TCount1953] Count is 4998
Thead[TCount3087] Count is 4999
Thead[TCount2425] Count is 5000
3 加锁机制
另外一种常见的同步办法就是加锁,比方 Java 中有一种重入锁 ReentrantLock,它是一种递归无阻塞的同步机制,绝对于 synchronized 来说,它能够提供更加弱小和灵便的锁机制,同时能够缩小死锁产生的概率。示例代码如下:
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,这样能够进行读写拆散,效率更高。
4 应用 Atomic 对象
因为锁机制会影响肯定的性能,而有些场景下,能够通过无锁形式进行实现。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
5 无状态对象
后面提到,线程不平安的一个起因就是多个线程同时拜访某个对象中的数据,数据存在共享的状况,因而,如果将数据变成独享的,即无状态(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;
}
}
6 不可变对象
后面提到,如果须要在多线程中共享一个数据,而这个数据给定值,就不能扭转,那么也是线程平安的,相当于只读的属性。在 Java 中能够通过 final 关键字进行属性润饰。上面给出示例代码:
package com.example.learn;
public class Counter {
public final int count ;
public Counter (int n) {count = n;}
}
7 总结
后面提到了几种线程平安的办法,总体的思维要不就是通过锁机制实现同步,要不就是避免数据共享,避免在多个线程中对数据进行读写操作。另外,有些文章中说到,能够在变量前应用 volatile 润饰,来实现同步机制,但这个通过测试是不肯定的,有些场景下,volatile 仍旧不能保障线程平安。尽管上述是线程平安的经验总结,然而还是须要通过严格的测试进行验证,实际是测验真谛的唯一标准。
点击关注,第一工夫理解华为云陈腐技术~