Java线程安全基础

Java的线程安全

多线程编程中的核心概念:

多线程编程中我们需要同时关注可见性、顺序性和原子性问题

原子性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。就是一个线程对变量进行的任何操作,所有共享了这个变量的其他线程都可以看到。

顺序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。CPU有可能不会按照我们写的代码的顺序来执行程序。

Java如何解决多线程并发中出现的问题:

只要保证了原子性,可见性和顺序性,就能够解决Java多线程并发中出现的问题。

Java如何保证原子性:

  1. 锁和同步:常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。

public void testLock () {
    //加锁
    lock.lock();
    try{
       int j = i;
       i = j + 1;
    } finally {
       //释放锁
       lock.unlock();
    }
}

与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。下面是同步代码块示例


public void testLock () {
    synchronized (anyObject){
        int j = i;
        i = j + 1;
    }
}

java如何保证可见性:

Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

Java保证顺序性:

上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

volatile适用场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。

happen-before 原则

传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取volatile
变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
程序次序规则:一个线程内,按照代码顺序执行
线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
线程终结原则:线程的终止检测后发生于线程中其它的所有操作
线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取对象
终结规则:一个对象构造先于它的finalize发生

你可能感兴趣的:(Java)