线程安全

1.JVM运行时数据区

在这里插入图片描述
线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁
线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁

2. Java内存模型VS JVM运行时数据区

在这里插入图片描述

3.初看Java内存模型

  • 前面章节中的大部分讨论仅涉及代码的行为,即一次执行单个语句或表达式,
    即通过单个线程来执行。Java虚拟机可以同时支持多个执行线程,若未正确同步,线程的行为可能会出现混淆和违反直觉。
  • 本章描述了多线程程序的语义;它包含了,当多个线程修改了共享内存中的值时,应该读取到哪个值的规则。由于这部分规范类似于不同硬件体系结构的内存模型,因此这些语义称为Java编程语言内存模型。
  • 于这部分规范类似于不同硬件体系结构的内存模型,因此这些语义称为Java编程语言内存模型。

4. 多线程中的问题

  1. 所见非所得
  2. 无法肉眼去检测程序的准确性
  3. 不同的运行平台有不同的表现
  4. 错误很难重现
public class DemolVisibility {
    int i=0;
    boolean isRunning=true;
    public static void main(String args[]) throws  InterruptedException{
        DemolVisibility demo=new DemolVisibility();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (demo.isRunning){
                    demo.i++;
                }
                System.out.println("i="+demo.i);
            }
        }).start();
        Thread.sleep(3000L);
        demo.isRunning=false;
        System.out.println("shutdown....");
    }

}

在这里插入图片描述

在这里插入图片描述

5.CPU指令重排序

Java编程语言的语义允许Java编译器和微处理器进行执行优化,当多线程的时候这些优化导致了与其交互的代码不再同步,从而导致看似矛盾的行为。
在这里插入图片描述

6.JIT编译器(Just In Time Compiler)

脚本语言与编译语言的区别?
解释执行:即咱们说的脚本,在执行时,由语言的解释器将其一条条翻译成机器可识别的指令。编译执行:将我们编写的程序,直接编译成机器可以识别的指令码。
Java是脚本语言还是编译语言? Java介于脚本语言与编译语言之间
在这里插入图片描述

7.volatile关键字

可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
Java内存模型规定:
对volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以volatile关键字就有这些功能:

  1. 禁止缓存;
    volatile变量的访问控制符会加个ACC_VOLATILE
    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
    1417015287
  2. 对volatile变量相关的指令不做重排序;

8. Shared Variables定义

可以在线程之间共享的内存称为共享内存或堆内存。
所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享变量。
′冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量是内存模型规范的对象。

9.线程间操作的定义

  1. 线程间操作指:一个程序执行的操作可被其他线程感知或被其他线程直接影响.
  2. Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。
    操作间操作有:

read操作(一般读,即非volatile读)
write操作(一般写,即非volatile写)
volatile read
volatile write
Lock.(锁monitor)、Unlock>
线程的第一个和最后一个操作
外部操作
所有线程间操作,都存在可见性问题,JMM需要对其进行规范

10.对于同步的规则定义

对volatile变量v的写入,与所有其他线程后续对v的读同步
对于监视器m的解锁与所有后续操作对于m的加锁同步
对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同步
启动线程的操作与线程中的第一个操作同步
线程T2的最后操作与线程T1发现线程T2已经结束同步。( isAlive ,join可以判断线程是否终结)
如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步
通过抛出InterruptedException异常,或者调用Thread.interrupted或 Thread.isInterrupted

11.Happens-before先行发生原则

happens-before关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before另
一个action,则第一个操作被第二个操作可见,JVM需要实现如下happens-before规则:
某个线程中的每个动作都 happens-before该线程中该动作后面的动作。
某个管程上的unlock动作happens-before同一个管程上后续的lock动作
对某个volatile 字段的写操作 happens-before每个后续对该volatile字段的读操作在某个线程对象上调用start()方法 happens-before被启动线程中的任意动作如果在线程t1中成功执行了t2.join(),则t2中的所有操作对t1可见
如果某个动作 a happens-before动作 b,且 b happens-before动作c,则有 a happens-before c.\

12. final在JMM中的处理

final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。伪代码示例: f= new finalDemo();读取到的f.x一定最新,x为final字段。
如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;伪代码示例:public finalDemo(){x= l; y=x;}; y会等于l;
读取该共享对象的final成员变量之前,先要读取共享对象。
伪代码示例:r= new ReferenceObj(); k= r.f ;这两个操作不能重排序
通常被static final修饰的字段,不能被修改。然而System.in、System.out、System.err被static final修饰,却可以修改,遗留问题,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段;

13. double和long的特殊处理

由于《Java语言规范》的原因,对非 volatile的double、long的单次写操作是分两次来进行的,每次操作其中32位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。
32位
32位
64位long/double

在这里插入图片描述

14 原子操作

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。
在这里插入图片描述

14.1 CAS ( Compare and swap )

Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。
JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS

在这里插入图片描述

14.2 ABA问题

举个例子:
主内存有个数据值:A,两个线程A和B分别copy主内存数据到自己的工作区,A执行比较慢,需要10秒, B执行比较快,需要2秒, 此时B线程将主内存中的数据更改为B,过了一会又更改为A,然后A线程执行比较,发现结果是A,以为别人没有动过,然后执行更改操作。其实中间已经被更改过了,这就是ABA问题。
也就是ABA问题只要开始时的数据和结束时的数据一致,我就认为没改过,不管过程。

尽管A线程的CAS操作是成功的,但是不代表这个过程就是没问题的。

ABA问题说简单点就是,预判值还是和当初抓取的一样,但是这个“ 值 ”的版本可能不一样了,在某些不仅要考虑数据值是否一致,还要考虑版本是否一致的场景下需要注意.

Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
在这里插入图片描述

thread1、thread2同时读取到 i=0后,
thread1、thread2都要执行CAS(0,1)操作,
假设thread2操作稍之后与thread1,则thread1执行成功> threadl紧接着执行了CAS(1,0),将i的值改回0

15 JAVA中锁的概念

自旋锁: 是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
乐观锁∶ 假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
独享锁(写): 给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
共享锁(读)∶给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)
可重入锁、不可重入锁: 线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
公平锁、非公平锁: 争抢锁的顺序,如果是按先来后到,则为公平。

15.1 同步关键字synchronized

1、用于实例方法、静态方法时,隐式指定锁对象
2、用于代码块时,显示指定锁对象
3、锁的作用域:对象锁、类锁、分布式锁
4、引申:如果是多个进程,怎么办?
特性:可重入、独享、悲观锁
锁优化︰锁消除(开启锁消除的参数:-XX:+DoEscapeAnalysis-XX:+EliminateLocks)

15.2 锁的状态如何记录

synchronized ( this){
i++;
}

如上面的代码 状态会被记录到this对象中吗?
若锁占用,线程挂起,
释放锁时,唤醒挂起的线程,是如何做到的?
1…对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
其中类对象存储如下图:
在这里插入图片描述

public class Demo5_main {

public static void main (string args[]){
int a - 1;
Teacher james = new Teacher () ;
james.stu = new Student();
}}
class Teacher(

String name = "james" ;

int age = 40;
boolean gender = true;

student stu;


static....

}}


class student(
String name -""Emily";int age = 18;
boolean gender = false;
}


在这里插入图片描述
堆中存储的是实例对象的值
String str=System.getEnv(“JAVA_HOME”)
String类型是引用类型存在堆的形式是以字符串数组形式
一种形式是String str=“”a“”;
局部变量存的是堆对象的引用,堆中存的是常量池中的引用。
方法是行为需要的时候执行。方法是方法区是类元数据的一部分
static修饰的一部分作为不是对象,做为元数据的一部分存在方法区中。
方法是行为需要的时候执行。方法是方法区是类元数据的一部分
static修饰的一部分作为不是对象,做为元数据的一部分存在方法区中。

15.3 Mark Word(标记字)

在这里插入图片描述

以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:

在这里插入图片描述
其中以上实例对应的如图:

在这里插入图片描述

15.4 锁的升级过程

在这里插入图片描述

15.5 偏向锁

|在JDK6以后,默认已经开启了偏向锁这个优化,通过JVM参数-XX:-UseBiasedLocking来禁用偏向锁
若偏向锁开启,只有一个线程抢锁,可获取到偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。
当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,
以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置为1(表示指向当前进程):
如果没有,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前进程。
——《Java并发编程的艺术》
在这里插入图片描述

15.6 自旋锁(轻量级锁)

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁

轻量级锁的加锁过程:

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中;
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

15.7 重量级锁

轻量级锁膨胀之后,就升级为重量级锁了。
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。

synchronized就是一个典型的重量级锁 synchronized关键字

16 Locks包类层次结构

在这里插入图片描述

16.1 Lock接口

在这里插入图片描述

16.1.1死锁案例
 static Lock lock =  new ReentrantLock();  //可重入锁
    static Object baozidian=null;

    static   int i=0;
    public static void main(String args[]) throws InterruptedException {
        lock.lock();    //当前线程已获取锁
        System.out.println("get lock 1...");

        lock.lock();    //再次获取,是否能成功
        System.out.println("get lock 2...");

        lock.unlock();
      //  lock.unlock();
        new Thread(()->{
            System.out.println("child");
            lock.lock();
            System.out.println("get lock... child");
        }).start();
        }

在这里插入图片描述
ReentrantLock是可重入锁,只是只释放一个。
在这里插入图片描述

首先t2获取锁,count加1 owner指向t2,t2线程在加锁就加1,t3这时去获取锁,先判断count是否为大于1,大于在判断owner是否属于t3这时不使用cas操作,如果不是属于t3进行放入等待队列,直到t2线程释放锁为0的时候唤醒t3线程,进行cas(0,1)操作这时如果t4也加入锁会进行抢锁,因为ReentrantLock是不公平锁,如果t3抢到就t3出列进行加锁,t4进入等待队列。reentrantLock(true)可变为公平锁

wait/notify使用了synchroinzed中用

16.1.2 synchronized vs Lock

Synchronized
优点:

  1. 使用简单,语义清晰,哪里需要点哪里。
  2. 由JVM提供,提供了多种优化方案(锁粗化、锁消除、偏向锁、轻量级锁)3、锁的释放由虚拟机来完成,不用人工干预,也降低了死锁的可能性
    缺点:无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等
    Lock
    优点:
  3. 所有synchronized的缺点
  4. 可以实现更多的功能,让synchronized缺点更多
    缺点:
    需手动释放锁unlock,新手使用不当可能造成死锁

17.读写锁

概念
维护一对关联锁,一个只用于读操作,一个只用于写操作。
读锁可以由多个读线程同时持有,写锁是排他的。同一时间,两把锁不能被不同线程持有。
适用场景
适合读取操作多于写入操作的场景,改进互斥锁的性能
,比如:集合的并发线程安全性改造、缓存组件。
锁降级
指的是写锁降级成为读锁。持有写锁的同时,再获取读锁,随后释放写锁的过程。
写锁是线程独占,读锁是共享,所以写->读是降级。(读->写,是不能实现的)

AQS抽象队列同步器
在这里插入图片描述
aqs 提供了对资源占用、释放,线程的挂起、唤醒的逻辑。
预留了各种try方法给用户实现
可以用在各种需要控制资源争用的场景中。(ReentrantLock/CountDownLatch/Semphore)

,还有以下几个同步类是通过AQS的同步器进行同步管理的,不同的地方在于tryAcquire-tryRelease的实现方式不一样

ReentrantLock:可重入锁,与mutex一样实现了Lock接口,使用独占模式的同步器。
CountDownLatch:计数器,使用了共享模式的同步器进行多线程执行控制。
Semphore:一个计数信号量,维持一个许可证池(只计数)每次执行前获取许可证,执行完释放许可证。类似限流算法中的令牌桶算法。

ReadWriteLock
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值