【Java并发】(3)线程安全


参考

  1. 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
  2. 《Java并发编程的艺术》
  3. 《Java高级程序员面试笔试宝典》
  4. CyC2018/CS-Notes
  5. 面试官:说说什么是线程安全?一图带你了解java线程安全

1. Java内存模型

  • Java 内存模型(Java Memoty Model,JMM)可以屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果,从而使Java的并发编程达到跨平台的效果。如果使用C语言等直接对操作系统的内存模型进行操作,可能需要编写不同的代码。
  • Java线程之间的数据通信需要依靠共享变量来实现
    在这里插入图片描述
    如图所示,所有的共享变量存储在主内存中,每条线程还有自己的工作内存,线程对共享变量的操作(读写等)需要先拷贝(拷贝的是对象引用、需要操作的对象的成员变量,不会拷贝整个对象)到自己的工作内存中,而不能直接读写主内存中的数据,操作完成后,可以将该变量的值同步回主内存的共享变量中,对共享变量的读写需要通过JMM来完成,不同的线程之间也无法直接访问对方工作内存中的变量。
  • 由于多个线程需要操作共享内存,由此引发了线程安全问题

2. 线程安全

  • 线程不安全的实例
public class ThreadUnSafe {
    public static int sharedVar = 0;

    public static void main(String[] args) {
        ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            threadPoolExecutor.execute(() -> {
                for (int i1 = 0; i1 < 10000; i1++) {
                    sharedVar++;
                }
            });
        }
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {

        }
        System.out.println(sharedVar);//输出小于20000的值
    }
}
这个例子中,开启了两个线程,每个线程对共享变量sharedVar的值增加10000次,
期望的输出为20000,但结果输出不定,是一个小于20000的数字。
因为执行过程可能为线程1取值为1->线程1计算结果为2->线程2取值为1->线程2计算结果为2->线程1存值为2->线程2存值为2。即线程2在线程1的计算结果写之前又读了这个值,造成结果错误。
  • 线程安全的定义
  1. 《Java并发编程实战(Java Concurrency In Practice)》中的定义:“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对 象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”
  2. 通俗的理解:在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改而发生不期望的情况。
  • 线程安全的三大特性
    Java内存模型是围绕着在并发过程中如何处理原子性可见性有序性这三个特征来建立的,这三个特征是线程安全的必备三要素,JMM提供了一系列的途径来满足这三个特征。
  1. 原子性:一个多个操作,要么全部连续执行且不会被任何元素中断,要么就不执行。
    对于单个操作来说,JMM保证基本数据类型的单个操作(访问、读写)都是具备原子性的(long和double是例外)。
    对于多个操作来说,JMM提供机制来确保原子性。
  2. 可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
    JMM提供volatilefinal、锁来保证可见性。
  3. 有序性:程序执行的顺序应当按照代码的先后顺序执行。如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。(本线程内部的代码,可以重排列,不会对本线程的结果造成影响,但可能对其他线程造成不期望的影响)。JMM提供volatile禁止指令重排,保证持有同一个锁的两个同步块只能串行地进入。

3. Synchronized

  • synchronized为指定对象加锁,如果一个线程需要进入一个同步方法或同步代码块,需要先获取锁,一个对象只有一个锁
  • 使用synchronized修饰方法
  1. 修饰实例方法
public synchronized void f(){}

此时加锁的对象是类的实例对象,等价于

public void f(){
	synchronized(this){}
}

其他线程需要进入这样的同步代码块或该类的同步实例方法时,需要先获取类的实例对象的锁。如果不需要进入同步代码块或同步实例方法就不需要获取锁。
2. 修饰类方法

class A{
	public static synchronized void f(){}
}

此时加锁的对象是类的class对象,等价于

class A{
	public static void f(){
		synchronized(A.class){}
	}
}

其他线程需要进入这样的同步代码块或该类的静态同步实例方法时,需要先获取类的class对象的锁。

  • 使用synchronized为任意对象加锁
class A{
	Object o = new Object();
	public void f(){
		synchronized(o){}
	}
}

其他线程需要进入该代码块时,需要先获取o对象的锁。

  • 总之:
  1. 一个对象只有一把锁,对于同步代码块,线程必须获取锁才能进入。
  2. synchronized具有可重入性,已经获取锁的线程,可以直接再次进入同步代码块。
  • Synchronized可以保证线程安全的原子性可见性有序性
  1. 原子性:被synchronized修饰的代码块包含多个操作,因为有锁存在,这些操作不会被其他线程打断,因此满足原子性。
  2. 可见性:如果对一个共享变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
  3. 有序性:需要同一个锁的多个线程进入同步代码块时,是顺序进入的,同步代码块内部可能会进行指令重排列,但不会对其他线程的结果造成影响

4. Volatile

  • volatile可以满足轻量级的线程安全,在一定程度上,低于锁的开销。
  • volatile保证有序性可见性,不保证原子性
  1. 有序性:volatile可以禁止指令重排,被volatile修饰的变量,在代码块中,会保证执行的次序。
语句1
语句2
含有volatile变量的语句
语句3
语句4

其中含有volatile变量的语句一定在语句1,2之后执行,先于语句3,4执行,但语句1,2,语句3,4可能会被重排列。
例:单例模式的双重校验锁使用到了volatile的禁止指令重排功能
Java23种设计模式总结

  1. 可见性:当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值,而普通变量则不能保证这一点。volatile保证可见性的原理是内存屏障(当CPU写数据时,如果发现一个变量在其他CPU中存有副本,那么会发出信号通知其他CPU将该副本对应的缓存行置为无效状态,当其他CPU读取到变量副本的时候,会发现缓存行是无效的,然后它会从主存重新读取。)
public class TestVolatile {
    public int i = 0;

    Runnable setRunnable = () -> i = 1;

    Runnable getRunnable = () -> {
        while (i!=1) {
        }
    };

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(40, 40, 1, TimeUnit.SECONDS,new ArrayBlockingQueue<>(40));

        for(int i = 0 ; i < 20 ;i++){
            TestVolatile testVolatile = new TestVolatile();
            threadPool.execute(testVolatile.getRunnable);
            threadPool.execute(testVolatile.setRunnable);
        }
        threadPool.shutdown();
        while (!threadPool.isTerminated()) {
            System.out.println(threadPool.getActiveCount());
        }
    }
}

这个例子证明了volatile的可见性。每个TestVolatile的实例包含两个任务(设置i的值为1,无限循环检测i值是否为1),期望情况下,setRunnable修改i的值为1后,getRunnable就检测到i的值为1停止循环,但在并发情况下,getRunnable可能不会立即检测到i的值发生变化。
这里生成了20个对象,40个任务(线程),期望情况下,40个线程全部执行完毕,但在i的值不用volatile修饰时,总会有线程进入死循环。
如果加上volatile修饰,就可以满足期望的情况,40个线程全部执行完成。
(另外,如果线程数较少,可能不会出现线程进入死循环的情况。)

5. Java实现线程安全的方法

6. 锁优化

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值