Java编程:并发编程之线程安全

本文首发于我的公众号码农之屋(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所示。

图1 UnSafeStates类运行结果

 

出现此类情况的原因有很多种,最常见是线程1进入方法后拿到states值,还未改变其值,结果线程2也进入了,导致2线程拿到的states值是一样的(可参看图2的处理流程)。而这个结果也表明了UnSafeStates类的getStates()方法不是线程安全的。

图2 UnSafeStates.getStates()的错误执行情况

 

根据这个示例,我们总结下什么是线程安全:当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么就可以说这个类是线程安全的。


二、如何解决线程安全性问题

既然存在线程安全性问题,那么肯定需要有对应的方案来解决这个问题,接下来我们介绍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。

图3 添加synchronized内置锁后的运行结果

 

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是否能解决线程安全问题,具体运行结果如下。

图4 添加Lock锁后的运行结果

 

根据运行的结果表示,使用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++编程技术等内容,欢迎大家关注。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值