synchronized:保证可见性、原子性、(原子性保证指令重拍后结果不变)

volatile:保证可见性、禁止指令重排序

1.java中内存可见性问题

JMM规定,所有变量都放在内存中,线程使用变量时,从内存中复制一份变量到线程工作空间,线程读写变量都是操作自己工作空间内部变量。

由JMM可知,当线程A修改了变量C的值后,只修改了其本地内存的值,此时线程B并不知道线程A修改了变量C的值。导致变量C的值修改后不可见。

共享内存不可见原因:

与cpu多级缓存命中有关。

 JMM工作内存对应实际内存:

JMM中对应的线程工作内存,对应CPU内部的L1、L2缓存、寄存器。

2. 如何解决内存可见性问题?

2.1 synchronized

java内置锁,又称为监视器锁,任何对象都可以作为锁对象使用。线程的执行代码在进入synchroni ed 代码块前会自动获取内部锁,这时候其他线程访同步代码块 会被阻塞挂起。

释放内置锁情况:

线程正常退出;异常退出;线程调用wait方法等;

内存语义:

  • 进入 synchronized 块的内存语义是把在 synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变 时就不会从线程的工作内存中获取,而是直接从主内存中获取
  • 退出 synchronized 块的内存语义是把在synchronized块内对共享变修改刷新到主内存

此外,synchronized还可用于实现原子性操作,但该关键字会引起线程上下文切换。

2.2 volatile

volatile可以实现内存可见性,但是无法实现原子性操作。

3.java中的原子性操作

3.1 java如何进行原子性操作

(1)synchronized

synchronized关键字,具备原子性操作。但是synchronized属于锁操作,且时排他锁,多线程操作时,会导致线程阻塞挂起,降低并发性。

(2)非阻塞CAS算法实现的原子类

由于synchronized原子操作会导致线程阻塞挂起,增加开销,因此java还提供了无需线程挂起的CAS算法的实现类。

JDK提供的CAS算法实现类,通过硬件保证比较-交换的原子性。

CAS理解:当且仅当预期值A内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。

3.2 CAS

(1)CAS原理:

CAS通过调用Unsafe类的JNI(java本地native方法)代码实现。而unsafe类底层则借助C来调用CPU底层指令实现。

CPU底层指令如何实现???。。。留个坑

(2)CAS存在的问题:

ABA问题。线程A改变了变量V的值为V1,又改变为原来的值,此时线程B对这种改变操作未知。

ABA问题解决方案:加入版本号即可。 JDK 中的AtomicStampedReference类加入了时间戳,可以解决ABA问题。

4. java指令重排序

JMM允许编译器、CPU对指令重拍以提高运行性能,且只会对不存在数据依赖性的指令重排序。单线程情况下保证程序最终运行结果和程序顺序执行结果一致。

禁止指令重排:

synchronized:这种加锁的方式操作共享内存,多线程下指令重排序不会影响程序最终运行结果。

volatile:volatile关键字会禁止指令重排序。volatile写之前的操作不会被重排到写之后,volatile读之后的操作不会被重排到读之前。

5.伪共享

5.1 基本概念:

cpu cache:物理cpu存在多级缓存。而cache的存储按照行存储的,每一行称为cache行。cache行是cache与主存进行数据交换的单位,一般为2的幂次方。

缓存命中:cpu访问变量时,首先从cache 行中获取变量,如果存在则读取,否则从下一级或主存中刷入数据。

5.2 伪共享:

将多个变量同时放入一个cache行中,当多个线程同时修改cache行中的多个变量时,由于同时只能有一个线程操作cache行,相比于每个变量都放入一个cache行,性能会有所下降。

例如:变量x,y同时放入cache行中,cpu1写入x会导致cpu2更新对应的cache行,cpu2写入y会导致CPU1更新对应的cache行。导致一级缓存失效。

为什么会出现伪共享?

多个变量存储于一个缓存行内导致,而缓存与内存交换数据的单位就是缓存行,当cpu要访问的变量没有在缓存行中找到时,根据局部性原理,会把该变量所在内存中大小缓存行放入缓存中。

如何避免缓存行?

JDK8之前一般使用字节填充避免缓存行。字节填充方式,让每一个变量都单独存储于一个缓存行中。例如如下的FilledLong类型对象占用64字节,正好一个缓存行大小:

public final static class FilledLong{ 
    public volatile long value = 0L; 
    public long p1,p2,p3,p4,p5,p6; 
}

JDK8提供了sun.misc.Contended注解解决伪共享问题。该注解也可以注释变量,共享大小可以通过- XX:-RestrictContended和-XX Con nd dPaddingWidth设置。

@sun.misc.Contended 
public final static class FilledLong{ 
    public volatile long value = 0L; 
}

6.锁概述

6.1 乐观锁、悲观锁

悲观锁:

悲观锁认为,数据很容易被其他线程修改,因此处理数据之前先对数据加锁,在整个数据处理过程中,使数据处于锁定状态。

乐观锁:

乐观锁 相对悲观锁来说的,它认为 据在一般情况下不会造成冲 ,所以在访问记录前不会加排它锁,而 在进行数据提交更新时,才会正式对数据冲 与否进行检测

6.2 公平锁、非公平锁

两者区别在于,获取锁的顺序是否按照请求锁的时间决定的。公平锁的使用,相比于非公平锁更加消耗性能。

非公平锁实现时,当前线程释放锁后,直接通过循环cas再次获取锁,获取到以后不用进行线程上下文切换,消耗较低。

公平锁实现时,通过队列实现,每次获取引起线程上下文切换,消耗性能。

ReentrantLock pairLock = new ReentrantLock(true); 
ReentrantLock unpairLock = new ReentrantLock(false);

6.3 独占锁、共享锁

根据锁只能被单个线程持有还是能被 个线程共同持有,锁可以分为独占锁和共享锁。前者归属于悲观锁,后者归属于乐观锁。

 ReentrantLock是独占锁, ReadWriteLock是共享锁,允许一个资源可以同时被多个线程进行读操作。

6.4 可重入锁

当一个线程要获取 个被其他线程持有的独占锁时,该线程会被阻塞。当线程再次获取它自己己经获取的锁时,如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严格来说是有限次数)地进入被该锁锁住的代码.

synchronized是可重入锁。

6.5 自旋锁

自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10 ,可以使用-XX:PreBlockS pin 数设置该值),很有可能在后面几次尝试中其他线程己经释放了锁,如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。

自旋锁通过自旋的方式,一直占用cpu来换取线程阻塞与调度的开销。


评论关闭
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

6. ThreadLocal