关于java:1到底什么是线程安全和线程安全的实现

2次阅读

共计 6204 个字符,预计需要花费 16 分钟才能阅读完成。

1. 什么是线程平安?

维基百科给出的定义如下:

线程平安是程式设计中的术语,指 某个函数、函数库 多线程环境 中被调用时,可能正确地解决多个线程之间的 共享变量,使程序性能正确实现。

在《Java 并发编程实战》一书中给出如下定义:

一个对象是否须要是线程平安的,取决于它是否被多个线程拜访。这只和对象在程序中是以何种形式被应用的无关,和对象自身具体是做什么的无关。

在《深刻 Java 虚拟机》一书中给出如下定义:

当多个线程拜访同一个对象时,如果不必思考这些线程在运行时环境下的调度和交替运行,也不须要进行额定的同步,或者在调用方进行任何其余的协调操作,调用这个对象的行为都能够获取正确的后果,那这个对象是线程平安的。

线程平安:在多线程同时拜访一个资源时,线程间按照某种形式拜访资源时,拜访的后果总是能获取到正确的后果。

2.Java 内存模型 -JMM

上图形容了一个多线程执行场景。线程 A 和线程 B 别离对主内存的 变量 进行读写操作。其中 主内存 中的 变量 共享变量 , 也就是说此变量只此一份,多个线程间共享。然而线程不能间接读写主内存的 共享变量 ,每个线程都有本人的 工作内存 ,线程须要读写主内存的 共享变量 时须要先将该变量拷贝一份正本到本人的工作内存,而后在本人的工作内存中对该变量进行所有操作,线程工作内存对变量正本实现操作之后须要将后果同步至主内存。

线程的工作内存是线程公有内存,线程间无奈相互拜访对方的工作内存。

3. 共享变量(共享资源)

所谓共享变量,指的是多个线程都能够操作的变量。过程是分配资源的根本单位,线程是执行的根本单位。所以,多个线程之间是能够共享一部分过程中的数据的。在 JVM 中,Java 堆和办法区的区域是多个线程共享的数据区域。也就是说,多个线程能够操作保留在堆或者办法区中的同一个数据。那么,保留在堆和办法区中的变量就是 Java 中的共享变量。

那么,Java 中哪些变量是寄存在堆中,哪些变量是寄存在办法区中,又有哪些变量是寄存在栈中的呢?

Java 中共有三种变量,别离是类变量、成员变量和局部变量。他们别离寄存在 JVM 的办法区、堆内存和栈内存中。(栈内存是程序线程独占空间)

public class Variables {

    /**
     * 类变量
     */
    private static int a;

    /**
     * 成员变量
     */
    private int b;

    /**
     * 局部变量
     * @param c
     */
    public void test(int c){int d;}
}

下面定义的三个变量中,变量 a 就是类变量,变量 b 就是成员变量,而变量 c 和 d 是局部变量。

所以,变量 a 和 b 是共享变量,变量 c 和 d 是非共享变量。所以如果遇到多线程场景,对于变量 a 和 b 的操作是须要思考线程平安的,而对于线程 c 和 d 的操作是不须要思考线程平安的。

4. 线程平安的实现

4.1 无状态实现

在大多数状况下,多线程利用中的谬误是谬误地在多个线程之间共享状态的后果。

因而,咱们要钻研的第一种办法是 应用无状态实现 来实现线程平安。

为了更好地了解这种办法,让咱们思考一个带有静态方法的简略工具类,该办法能够计算数字的阶乘:

public class MathUtils {public static BigInteger factorial(int number) {BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}

factorial办法是一种无状态确定性函数。 确定性是指:给定特定的输出,它将始终产生雷同的输入。

该办法 既不依赖内部状态,也不保护本身的状态。因而,它被认为是线程平安的,并且能够同时被多个线程平安地调用。

所有线程都能够平安地调用 factorial 办法,并且将取得预期后果,而不会相互烦扰,也不会更改该办法为其余线程生成的输入。

因而,无状态实现是实现线程平安的最简略办法

4.2 不可变的实现

如果咱们须要在不同线程之间共享状态,则能够通过使它们成为不可变对象来创立线程安全类

不变性是一个功能强大,与语言无关的概念,在 Java 中相当容易实现。

当类实例的外部状态在结构之后无奈批改时,它是不可变的

在 Java 中创立不可变类的最简略办法是申明所有字段为 privatefinal,且不提供 setter:

public class MessageService {
    
    private final String message;
 
    public MessageService(String message) {this.message = message;}
    
    // 规范 getter
    
}

一个 MessageService 对象实际上是不可变的,因为它的状态在结构之后不能更改。因而,它是线程平安的。

此外,如果 MessageService 实际上是 可变 的,然而多个线程仅对其具备 只读 拜访权限,那么它也是线程平安的。

因而,不变性是实现线程平安的另一种办法

4.3 线程公有 (ThreadLocal) 字段

在面向对象编程(OOP)中,对象实际上须要通过字段保护状态并通过一种或多种办法来实现行为。

如果咱们的确须要保护状态,则能够通过使它们的字段成为线程部分的来创立不在线程之间共享状态的线程安全类。

通过简略地在 Thread 类中定义公有字段,咱们能够轻松创立其字段为线程部分的类。

例如,咱们能够定义一个存储整数数组的 Thread 类:

public class ThreadA extends Thread {private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    @Override
    public void run() {numbers.forEach(System.out::println);
    }
}

而另一个类可能领有一个字符串数组:

public class ThreadB extends Thread {private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
    
    @Override
    public void run() {letters.forEach(System.out::println);
    }
}

在这两种实现中,这些类都有其本人的状态,然而不与其余线程共享。因而,这些类是线程平安的。

同样,咱们能够通过将 ThreadLocal 实例调配给一个字段来创立线程公有字段。

例如,让咱们思考以下 StateHolder 类:

public class StateHolder {
    
    private final String state;
 
    // 规范的构造函数和 getter
}

咱们能够很容易地使其成为线程部分(ThreadLocal)变量,如下所示:

public class ThreadState {public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {
        
        @Override
        protected StateHolder initialValue() {return new StateHolder("active");  
        }
    };
 
    public static StateHolder getState() {return statePerThread.get();
    }
}

线程部分字段与一般类字段十分类似,不同之处在于,每个通过 setter / getter 拜访它们的线程都将取得该字段的独立初始化正本,以便每个线程都有本人的状态。

4.4 同步汇合类

通过应用 collections 框架 中蕴含的一组同步包装器,咱们能够轻松地创立线程平安的 collections。

例如,咱们能够应用以下同步包装之一来创立线程平安的汇合:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();

让咱们记住,同步汇合在每种办法中都应用外在锁定(咱们将在前面介绍外在锁定)。

这意味着 该办法一次只能由一个线程拜访,而其余线程将被阻塞,直到该办法被第一个线程解锁。

因而,因为同步拜访的根本逻辑,同步会对性能造成不利影响。

4.5 反对并发的汇合

除了同步汇合,咱们能够应用并发汇合来创立线程平安的汇合。

Java 提供了 java.util.concurrent 包,其中蕴含多个并发汇合,例如 ConcurrentHashMap

Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");

与同步对象不同,并发汇合通过将其数据划分为段来实现线程平安。例如,在 ConcurrentHashMap 中,多个线程能够获取不同 Map 段上的锁,因而多个线程能够同时拜访 Map

因为并发线程拜访的先天劣势,并发汇合类 具备 远超同步汇合类更好的性能

值得一提的是,同步汇合和并发汇合仅使汇合自身具备线程安全性,而不使 content 变得线程平安

4.6 原子化对象

应用 Java 提供的一组原子类(包含 AtomicInteger,AtomicLong,AtomicBoolean 和 AtomicReference )也能够实现线程平安。

原子类使咱们可能执行平安的原子操作,而无需应用同步。原子操作在单个机器级别的操作中执行。

要理解解决的问题,让咱们看上面的 Counter 类:

public class Counter {
    
    private int counter = 0;
    
    public void incrementCounter() {counter += 1;}
    
    public int getCounter() {return counter;}
}

让咱们假如在竞争条件下,两个线程同时拜访 increasingCounter() 办法。

从实践上讲,counter 字段的最终值为 2。然而咱们不确定后果如何,因为线程在同一时间执行同一代码块,并且增量不是原子的。

让咱们应用 AtomicInteger 对象创立 Counter 类的线程平安实现:

public class AtomicCounter {private final AtomicInteger counter = new AtomicInteger();
    
    public void incrementCounter() {counter.incrementAndGet();
    }
    
    public int getCounter() {return counter.get();
    }
}

这是线程平安的,因为在 ++ 增量执行多个操作的同时,增量和获取 是原子的

4.7 同步办法

只管较早的办法对于汇合和基元十分有用,但有时咱们须要的控制权要强于此。

因而,可用于实现线程平安的另一种常见办法是实现同步办法。

简而言之,一次只能有一个线程能够拜访同步办法,同时阻止其余线程对该办法的拜访。其余线程将放弃阻塞状态,直到第一个线程实现或该办法引发异样。

咱们能够通过使它成为同步办法,以另一种形式创立线程平安版本的 creationCounter()

public synchronized void incrementCounter() {counter += 1;}

咱们通过与前缀的办法签名创立一个同步办法 synchronized 关键字。

因为一次一个线程能够拜访一个同步办法,因而一个线程将执行 crementCounter() 办法,而其余线程将执行雷同的操作。任何重叠的执行都不会产生。

同步办法依赖于“外部锁”或“监视器锁”的应用。固有锁是与特定类实例关联的隐式外部实体。

在多线程上下文中,术语 monitor 是指对关联对象执行锁的角色,因为它强制对一组指定的办法或语句进行排他拜访。

当线程调用同步办法时,它将获取外部锁。线程实现执行办法后,它将开释锁,从而容许其余线程获取锁并取得对办法的拜访。

咱们能够在实例办法,静态方法和语句(已同步的语句)中实现同步。

4.8 同步语句

有时,如果咱们只须要使办法的一部分成为线程平安的,那么同步整个办法可能就显得过分了。

为了阐明这个用例,让咱们重构 increascountCounter 办法:

public void incrementCounter() {
    // 此处可有额定不需同步的操作
    // ...
    synchronized(this) {counter += 1;}
}

该示例很简略,然而它显示了如何创立同步语句。假如该办法当初执行了一些不须要同步的附加操作,咱们仅通过将相干的状态批改局部包装在一个 同步 块中来对其进行 同步

与同步办法不同,同步语句必须指定提供外部锁的对象,通常是 this 援用。

同步十分低廉,因而应用此选项,咱们尽可能只同步办法的相干局部

4.8 其余对象作为锁

咱们能够通过将另一个对象用作监视器锁定,来略微改善 Counter 类 的线程平安实现。

这不仅能够在多线程环境中提供对共享资源的协调拜访,还能够应用内部实体来强制对资源进行独占拜访

public class ObjectLockCounter {
 
    private int counter = 0;
    private final Object lock = new Object();
    
    public void incrementCounter() {synchronized(lock) {counter += 1;}
    }
    
    // 规范 getter
}

咱们应用一个一般的 Object 实例来强制互相排挤。此实现稍好一些,因为它能够进步锁定级别的安全性。

将 this 用于外部锁定时,攻击者可能会通过获取外部锁定并触发拒绝服务(DoS)条件来导致死锁。

相同,在应用其余对象时,无奈从内部拜访该公有实体。这使得攻击者更难取得锁定并导致死锁。

5. 参考文章

1. 什么是线程平安?.https://www.jianshu.com/p/448…

2. 深刻了解 Java 并发编程(一):到底什么是线程平安.https://www.hollischuang.com/…

3.Java 并发根底——线程安全性.https://www.cnblogs.com/NeilZ…

4. 什么是线程平安以及如何实现?https://segmentfault.com/a/11…

5. 你真的晓得什么是线程平安吗?.https://www.hoohack.me/2020/0…

6.(解释的比较清楚,透彻的)图解 Java 线程平安.https://juejin.cn/post/684490…

7.Java 进阶(二)当咱们说线程平安时,到底在说什么.http://www.jasongj.com/java/t…

正文完
 0