Java线程安全以及线程安全的实现方式和内存模型(JMM)

一、了解几个概念

1)临界区:

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用

2)互斥量:

互斥量是一个可以处于两态之一的变量:解锁和加锁。这样,只需要一个二进制位表示它,不过实际上,常常使用一个整型量,0表示解锁,而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)需要访问临界区时,它调用mutex_lock。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。
另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。

3)信号量:

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

4)CAS操作(Compare-and-Swap):

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B

5)重排序:

编译器和处理器”为了提高性能,而在程序执行时会对程序进行的重排序。它的出现是为了提高程序的并发度,从而提高性能!但是对于多线程程序,重排序可能会导致程序执行的结果不是我们需要的结果!重排序分为“编译器”和“处理器”两个方面,而“处理器”重排序又包括“指令级重排序”和“内存的重排序”

二、Java内存模型(JMM)

线程与内存交互操作如下 

所有的变量(实例字段,静态字段,构成数组对象的 元素,不包括局部变量和方法参数)都存储在主内存中,每个线程有自己的工作内存,线程的工作内存保存被线程使用到变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量。不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递通过主内存来完成。

1、Java内存模型定义了八种操作

lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态;
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
read(读取):作用于主内存的变量,它把一个变量的值从主内存传送到线程中的工作内存,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎;
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量;
store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作;
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值写入主内存的变量中。

2、volatile关键字作用:

1)、当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
2)、当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
3)、禁止指令重排序优化。
4)、volatile关键字不能保证在多线程环境下对共享数据的操作的正确性。可以使用在自己状态改变之后需要立即通知所有线程的情况下,也就是说volatile不能保证线程同步,Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程,这就是所谓的线程可见性,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值,所以volatile具有可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入,然后synchronized也是具有可见性。

5)、volatile的原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性,所以volatile能保证可见性,不能保证原子性。

理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步

class VolatileFeaturesExample {
    volatile long vl = 0L;  //使用volatile声明64位的long型变量

    public void set(long l) {
        vl = l;   //单个volatile变量的写
    }

    public void getAndIncrement () {
        vl++;    //复合(多个)volatile变量的读/写
    }

    public long get() {
        return vl;   //单个volatile变量的读
    }
}

假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通变量

    public synchronized void set(long l) {     //对单个的普通 变量的写用同一个监视器同步
        vl = l;
    }

    public void getAndIncrement () { //普通方法调用
        long temp = get();           //调用已同步的读方法
        temp += 1L;                  //普通写操作
        set(temp);                   //调用已同步的写方法
    }
    public synchronized long get() {
    //对单个的普通变量的读用同一个监视器同步
        return vl;
    }
}

3 volatile和synchronized区别 
1)、volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;

2)、volatile读数据直接从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

3)、volatile能保证可见性,不能保证原子性;而synchronized则可以保证可见性和原子性;

4)、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

5)、volatile标记的变量不会被编译器优化进行指令重排列;synchronized标记的变量可以被编译器优化进行指令重排列。

三、JMM特性

1)、原子性

就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;

2)、可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改

3)、 有序性

Java内存模型中有序性可归纳为这样一句话:如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的

是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致

四、java中的线程安全等级

不可变
可以是基本类型的final;可以是final对象,但对象的行为不会对其状态产生任何影响,比如String的subString就是new一个String对象各种Number类型如BigInteger和BigDecimal等大数据类型都是不可变的,但是同为Number子类型的AtomicInteger和AtomicLong则并非不可变。原因与它里面状态对象是unsafe对象有关,所做的操作都是CAS操作,可以保证原子性。

绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。

相对线程安全
这是我们通常意义上的线程安全。需要保证对象单独的操作是线程安全的。比如Vector,HashTable,synchronizedCollection包装集合等。

线程兼容
对象本身不是线程安全的,但可以通过同步手段实现。一般我们说的不是线程安全的,绝大多数是指这个。比如ArrayList,HashMap等。

线程对立
不管调用端是否采用了同步的措施,都无法在并发中使用的代码。

五、线程安全的实现方式

要实现线程安全一般至少需要两个特性:原子性和可见性

1)使用synchronize:它本具有原子性和可见性的,所以如果使用了synchronize修饰的操作,那么就自带了可见性,synchronized使用悲观锁来实现线程安全

2)使用原子类代替基本数据类型,原子类是使用乐观锁来实现线程安全,多线程环境下执行a++,可以使用AtomicInteger类incrementAndGet()方法实现,同样是使用了volatile来保证可见性;使用Unsafe调用native本地方法CAS,CAS采用总线加锁或缓存加锁方式来保证原子性。

3 ) 使用volatile关键字,volatile不一定就有原子性,比如用volatile修饰的变量进行++或者--操作(num++),我们需要让volatile修饰的变量需要具有原子性,那么我们一般可以设置在boolean类型变量上,如下

volatile boolean tag = true;
线程1 while(tag){};
线程2 while(tag){};

如果有变量自增或者自减,我们可以使用原子类(AtomicInteger)

4)使用ThreadLocal对各个线程进行隔离

可以参考我的这篇博客 :Java之ThreaLocal

5)我们还可以用其他的锁,比如重入锁(ReentrantLock) 保证线程安全

6)我们还可以用 临界区、互斥量、信号量 保证线程安全

参考文献:

https://developer.51cto.com/art/201910/605093.htm

https://www.open-open.com/lib/view/open1459412319988.html

https://www.iteye.com/blog/smallbug-vip-2275743

https://juejin.im/post/5c936018f265da60ec281bcb

https://blog.csdn.net/jingzi123456789/article/details/78004074

(0)

相关推荐

  • Java开发之多线程的基本概念与如何避坑

    Java开发之多线程的基本概念与如何避坑

  • 7张图带你轻松理解Java 线程安全

    每日英文 Sometimes there is no next time, no second chance, no time out. Sometimes it is now or never. 有 ...

  • C语言丨深入理解volatile关键字

    本篇文章是对C语言中关键字volatile的含义进行了详细的分析介绍,希望能在学习上帮助大家.   volatile是一个类型修饰符(type specifier).它是被设计用来修饰被不同线程访问和 ...

  • Java并发编程之线程的创建

    简介 线程是基本的调度单位,它被包含在进程之中,是进程中的实际运作单位,它本身是不会独立存在.一个进程至少有一个线程,进程中的多个线程共享进程的资源. Java中创建线程的方式有多种如继承Thread ...

  • Java学习——114.交互线程

    线程在执行的过程中,有可能会需要共享资源或者交换数据,则称这一组线程为交互线程. 线程间的交互存在两种关系:竞争和协作. 竞争关系是指,两个线程同时使用同一个资源时,会产生竞争.此时,由于线程的并发执 ...

  • Java多线程(2):线程加入/join()

    线程加入 join()方法,等待其他线程终止.在当前线程(主线程)中调用另一个线程(子线程)的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态. 也就是主 ...

  • Android线程篇(五):Java内存模型

    作者:Android开发者社区 地址:https://www.jianshu.com/p/27a9003c33f4 声明:本文是 Android开发者社区 原创投稿,转发等请联系原作者授权. 终于等到 ...

  • 鸿蒙内核源码分析(线程概念篇) | 线程,线程,它到底长什么样?

    在鸿蒙内核线程(thread)就是任务(task),也可以叫作业.线程是对外的说法,对内就叫任务.跟王二毛一样, 在公司叫你王董,回到家里还有领导,就叫二毛啊.这多亲切.在鸿蒙内核是大量的task,很 ...

  • Android之HandlerThread源码分析和简单使用(主线程和子线程通信、子线程和子线程通信)

    Android之HandlerThread源码分析和简单使用(主线程和子线程通信、子线程和子线程通信)

  • 堆核到极致就堆线程:4线程SMT可能现身Zen 3霄龙EPYC处理器

    据AMD的路线图,Zen 3架构已经设计完毕,而Zen 4正在设计当中,使用这两种架构的霄龙EPYC 服务器处理器则分別名为Milan及Genoa.而随之而来,则是各种有关Zen 3的传言.外媒Har ...

  • Java 内存模型

    本书部分摘自<Java 并发编程的艺术> 线程通信与同步 在并发编程中,有两个需要处理的关键问题: 线程之间如何通信 线程之间如何同步 通信指线程之间以何种机制来交换信息,通信机制有两种: ...

  • Java 线程基础

    本文部分摘自<Java 并发编程的艺术> 线程简介 1. 什么是线程? 现代操作系统在运行一个程序时,会为其创建一个进程,一个进程里可以创建多个线程.现代操作系统调度的最小单元是线程,也叫 ...