Java如何实现多线程场景下的线程安全

举报
jackwangcumt 发表于 2021/08/10 10:21:48 2021/08/10
【摘要】 单线程可以正确运行的程序不代表在多线程场景下能够正确运行,这里的正确性往往不容易被发现,它会在并发数达到一定量的时候才可能出现。这也是在测试环节不容易重现的原因。因此,多线程(并发)场景下,如何编写线程安全(Thread-Safety)的程序,对于程序的正确和稳定运行有重要的意义。下面将结合示例,谈谈如何在Java语言中,实现线程安全的程序。

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依旧不能保证线程安全。虽然上述是线程安全的经验总结,但是还是需要通过严格的测试进行验证,实践是检验真理的唯一标准。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。