保证线程安全的几个小技巧

createh52周前 (04-11)技术教程5

前言

在软件编程中,多线程是个绕不开的话题。多线程的使用,能够提高程序的运行效率,但也带来新的问题:如何保证下面的线程安全呢?

无状态

例如:

public class Test {
public void threadMethod(int j) {
int i = 1;
j = j + i;
}

这个例子中,不存在全局变量,所以不存在线程安全问题。

官方解释:局部变量作用域仅限于函数内部, 离开该函数的内部就是无效的。

不可变

例如:

public class Test {
public static final String DEFAULT_NAME = "abc";

这个例子中,全局变量被final修饰,所以不存在线程安全问题。

官方解释:final被修饰的变量为常量一旦赋值不能修改,被修改的方法为最终方法不能被重写,被修饰的类是最终类,不能被继承

无修改权限

例如:

public class Test {
private String name;
public String getName() {
return name;
}
}

这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。

官方解释:

  • public 表示共有:类的数据成员和函数可以被该类对象和派生类访问。
  • private 私有型:自己的类可以访问,但派生类不能访问。
  • protected 保护型:自身类和派生类可以访问相当于自身的private型成员,它同private的区别就是在对待派生类的区别上。

synchronized

例如:

public class Test {
private int age = 18;


public synchronized int getAge1(int i) {
age = age + i;
return age;
}

public int getAge2(int i) {
synchronized(this){
age = age + i;
}
return age;
}

public int getAge3(int i) {
synchronized(Test.class){
age = age + i;
}
return age;
}
}

这个例子中,使用到了synchronized关键字,所以不存在线程安全问题。


官方解释: Synchronized可保证同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。


注意:

  1. 使用synchronized修饰非静态方法或者使用synchronized修饰代码块时指定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
  2. 使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。
  3. 使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。
  4. 线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

Lock

例如:

public class Test {

private ReentrantLock reentrantLock = new ReentrantLock();
private int age = 18;


public synchronized int getAge(int i) {
try{
reentrantLock.lock();
age = age + i;
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
return age;
}
}

这个例子中,使用到了ReentrantLock锁,所以不存在线程安全问题。


Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。

Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

分布式锁

public class Test {

private StringRedisTemplate redisTemplate;
private int age = 18;


public synchronized int getAge(int i) {
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
age = age + i;
}else{
getAge(i); //模拟自旋
}
return age;
}
}

这个例子中,使用到了rerdis的 setnx分布式锁,所以不存在线程安全问题。


如果你对redis分布式锁的用法和常见的坑,比较感兴趣的话,可以看看我的另一篇文章Redis缓存失效问题:缓存穿透-缓存雪崩-缓存击穿,有对分布式锁相关介绍说明

volatile

例如:

public class Test {
private volatile boolean running = false;
private Thread thread;

public void handle() {
//连接canal
while(running) {
//业务处理
}
}
public void start() {
thread = new Thread(this::handle, "name");
running = true;
thread.start();
}

public void stop() {
if(!running) {
return;
}
running = false;
}
}

这个例子中,全局变量使用volatile关键字,所以不存在线程安全问题。


官方解释:volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性。

注意:volatile不能用于计数和统计等业务场景。因为volatile不能保证操作的原子性,可能会导致数据异常。

ThreadLocal

例如:

public class Test {
private ThreadLocal threadLocal = new ThreadLocal<>();

public void getAge(int i) {
try {
Integer integer = threadLocal.get();
threadLocal.set(integer == null ? 0 : integer + i);
}catch (Exception e){
e.printStackTrace();
}finally {
threadLocal.remove();
}
}

}

这个例子中,使用了ThreadLocal,所以不存在线程安全问题。


官方解释:ThreadLoal 变量,线程局部变量,同一个ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本且其它 Thread 不可访问,那就不存在多线程间共享的问题。

注意:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在finally代码块中,调用它的remove方法清空数据,不然可能会出现内存泄露问题

线程安全集合

例如:

public class HashMapTest {

    private static ConcurrentHashMap hashMap = new ConcurrentHashMap<>();

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                hashMap.put("key1", "value1");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                hashMap.put("key2", "value2");
            }
        }).start();

        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(hashMap);
    }
}

这个例子中,使用了ConcurrentHashMap,所以不存在线程安全问题。


常见的线程安全集合还有:Vector, Hashtable, CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentSkipListMap, ConcurrentSkipListSet, ConcurrentLinkedQueue, ConcurrentLinkedDeque、使用Collections包装成线程安全等

CAS

public class Test {
private AtomicInteger atomicInteger = new AtomicInteger();

public int getAge(int i) {
return atomicInteger.getAndAdd(i);
}

}

这个例子中,使用了atomicInteger,所以不存在线程安全问题。


synchronized加锁,同一时间,只能有一个线程访问,一致性得到了保障,并发性下降。CAS用的do while,没有加锁,反复的通过CAS比较,直到成功,既保证了一致性,又提高了并发性。


逻辑上可以这么理解:java.util.concurrent.atomic 这个包里面提供了一组原子类,其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入。

相关文章

轻松掌握Java多线程 - 第四章:线程安全问题

学习目标1. 什么是线程安全1.1 线程安全的定义1.2 线程安全的重要性2. 共享资源访问的竞态条件2.1 什么是竞态条件2.2 竞态条件示例2.3 竞态条件的类型3. 线程安全问题的表现形式3.1...

如何在Java中实现线程安全?总结如下

在Java中,线程安全是指在多线程环境下,多个线程可以安全地访问共享资源或数据,而不会出现不一致或意外的结果。以下是一些实现线程安全的常用方法:1、使用synchronized关键字: 通过在方法或代...

Java多线程与锁机制详解:打造高效安全的并发世界

Java多线程与锁机制详解:打造高效安全的并发世界在当今这个数据处理量爆炸的时代,单线程程序已经难以满足高性能需求。Java作为一门优秀的编程语言,提供了强大的多线程支持,而锁机制正是保证多线程安全的...

Java集合框架的线程安全性:多线程编程的守护者

Java集合框架的线程安全性:多线程编程的守护者在Java的世界里,集合框架是所有开发者都绕不开的重要组成部分。无论是处理数据的存储还是操作,集合类几乎无处不在。然而,当我们把目光投向多线程编程的时候...

揭秘Java局部变量线程安全的真相:为什么它天生免疫并发问题

··在Java并发编程中,线程安全是一个永恒的话题。你是否曾疑惑:为什么局部变量不需要加锁就能避免并发问题?本文将深入剖析其底层原理,结合实战案例,带你彻底理解这一设计精髓。(点击收藏,解锁高薪面试必...

Java线程安全

当多个线程处理相同的数据,数据值发生变化时,会得到不一致的结果,这种情况不是线程安全的。 当一个线程已经在一个对象上工作并阻止另一个线程在同一个对象上工作时,这个过程称为线程安全。线程安全体现原子性:...