本文首发于我的公众号码农之屋(id: Spider1818),专注于干货分享,包含但不限于Java编程、网络技术、Linux内核及实操、容器技术等。欢迎大家关注,二维码文末可以扫。
导读:要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(可由多线程同时访问)和可变的(变量值在其生命周期内会发生变化)状态的访问。
一、什么是线程安全
本节我们将用一个示例来回答线程安全是什么,具体示例请参考以下实现代码。
public class UnsafeStates {
private int states = 0;
public int getStates() {
states++;
return states;
}
public static void main(String[] args) {
final UnsafeStates states = new UnsafeStates();
new Thread(){
public void run() {
for (int i = 0; i < 1000000; i++) {
System.out.println(states.getStates());
}
}
}.start();
new Thread(){
public void run() {
for (int i = 0; i < 1000000; i++) {
System.out.println(states.getStates());
}
}
}.start();
}
}
getStates()将UnSafeStates类的私有变量states递增后返回states值。我们启动了2个线程,每个线程循环调用1百万次getStates()方法,并将其返回值打印出来。按照我们的预期,线程间不应该产生影响,即最后运行的结果的最后一个值应为2000000。然后,最后的运行结果却是1999993(多次运行,结果还不一样,例如1999946等),具体输出结果请参考图1所示。
![](https://i-blog.csdnimg.cn/blog_migrate/1da5e47de17ef2407d0899c5dcedaadb.png)
出现此类情况的原因有很多种,最常见是线程1进入方法后拿到states值,还未改变其值,结果线程2也进入了,导致2线程拿到的states值是一样的(可参看图2的处理流程)。而这个结果也表明了UnSafeStates类的getStates()方法不是线程安全的。
![](https://i-blog.csdnimg.cn/blog_migrate/e3f22aecef3e37b9000d0bee854d9fc5.png)
根据这个示例,我们总结下什么是线程安全:当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么就可以说这个类是线程安全的。
二、如何解决线程安全性问题
既然存在线程安全性问题,那么肯定需要有对应的方案来解决这个问题,接下来我们介绍2种最常用的解决方案,更为详细的解决方案将放在另一篇章进行介绍。
1、内置锁synchronized
Java提供了一种内置锁机制来解决线程安全问题:synchronizedblock(同步代码块)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
每个Java对象都可以用作一个实现同步的锁,线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程1尝试获取一个由线程2持有的锁时,线程1必须等待或阻塞,直到线程2释放这把锁(如果线程2永远不释放锁,那么线程1将一直等下去)。
我们在此使用synchronized内置锁解决前面示例的线程安全问题,具体实现请参考以下实现代码。
public class UnsafeStates {
private intstates =0;
public synchronized int getStates() {
states++;
return states;
}
public static void main(String[] args) {
final UnsafeStates states = new UnsafeStates();
new Thread() {
public voidrun() {
for(int i = 0; i <1000000;i++) {
System.out.println(states.getStates());
}
}
}.start();
new Thread() {
public voidrun() {
for(int i = 0; i <1000000;i++) {
System.out.println(states.getStates());
}
}
}.start();
}
}
对getStates()方法添加synchronized内置锁后,线程间调用互不干扰,最后运行的结果的最后一个值为2000000,具体运行结果请参考图3。
![](https://i-blog.csdnimg.cn/blog_migrate/5d15e7ca7703fb50dd9fb0afd03d7875.png)
2、Lock
Lock是在Java 1.6被引进来的,跟synchronized内置锁相比,操作起来会比较复杂(Lock需要我们自己手动获取锁和释放锁,甚至可以中断获取以及超时获取同步特性)。还是以前面的示例为例,看看我们是如何使用Lock来保证线程安全的。
public class UnsafeStates {
private intstates =0;
private Lock lock =new ReentrantLock();
public intgetStates() {
lock.lock();
try{
states++;
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
return states;
}
public static void main(String[] args) {
final UnsafeStates states = new UnsafeStates();
new Thread() {
public void run() {
for(int i = 0; i <1000000;i++) {
System.out.println(states.getStates());
}
}
}.start();
new Thread() {
public void run() {
for(int i = 0; i <1000000;i++) {
System.out.println(states.getStates());
}
}
}.start();
}
}
进入方法后,首先要获取到锁,然后再执行业务代码。这里跟synchronized不同的是,Lock获取的锁对象需要我们亲自进行释放,为了防止我们代码出现异常,我们的释放锁操作放在finally中(finally中的代码无论如何都是会执行的)。我们通过多次实测数据,来验证下使用Lock是否能解决线程安全问题,具体运行结果如下。
![](https://i-blog.csdnimg.cn/blog_migrate/6e8e639bd060b0523983b866da4ddc5e.png)
根据运行的结果表示,使用Lock锁是可以解决线程安全问题的。其实Lock还有另外几种获取锁的方式,比如使用tryLock()方法获取锁。tryLock()方法跟Lock()方法是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。关于tryLock()方法的使用,在这就不再一一进行介绍了,后面将通过另一篇章进行详细的介绍。
我的公众号「码农之屋」(id: Spider1818) ,分享的内容包括但不限于 Linux、网络、云计算虚拟化、容器Docker、OpenStack、Kubernetes、SDN、OVS、DPDK、Go、Python、C/C++编程技术等内容,欢迎大家关注。