并发编程的其他基础知识
并发编程的其他基础知识
1. 什么是多线程并发编程
- 并发

- 并行

线程数多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
2. Java中的线程安全问题
操作共享变量时会出现脏数据或其他不可预见的结果。
典型的就是对计数器的操作。
3. Java中共享变量的内存可见性问题
-
Java内存模型
![image-20220723185959407]()
Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存。共享内存会存在共享变量内存不可见的问题。
Java中的volatile关键字可以解决内存不可见问题。
4. Java中的synchronized关键字
-
synchronized关键字介绍
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为內部锁,也叫监视器锁。内置所是排他锁。
另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作。
-
synchronized的内存语义
进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。
5. Java中的volatile关键字
使用volatile关键字可以解决内存可见性问题,这时一种Java提供的弱形式的同步。
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
volatile不保证原子性
volatile关键字的使用:
- 写入变量值不依赖变量的当前值。
- 读写变量值时没有加锁。
6. Java中的原子性操作
使用synchronized关键字可以实现线程安全性,即内存可见性和原子性,但是synchronized是独占锁,没有获取内部锁的线程会被阻塞掉。
7. Java中的CAS操作
CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。
8. Unsafe类
-
Unsafe类中的重要方法
Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。
-
long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。static{ try{ valueOffset = unsafe.objectFileOffset (AtomicLong.class.getDeclareField("value")); }cathch(Exception ex){throw new Error(ex);} } -
int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。
-
int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。
-
boolean compareAndSwapLong(Object obj,long offset,long expect,long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。
-
public native long getLongvolatile(Object obj,long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。
-
void putLongvolatile(Object obj,long offset,long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。
-
void putOrderedLong(Object obj,long offset,long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
-
void park(boolean isAbsolute,long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。
-
void unpark(Object thread)方法:唤醒调用park后阻塞的线程。
-
long getAndSetLong(Object obj,long offset,long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
-
long getAndAddLong(Object obj,long offset,long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue。
-
-
如何使用Unsafe类
public class TestUnsafe{ //获取Unsafe的实例(1) static final Unsafe = Unsafe; //记录变量state在类TestUnSafe中的偏移值(2) static final long stateOffset; //变量3 private volatile long state = 0; static { try{ //使用反射获取UnSafe的成员变量theUnsafe Field field = Unsafe.class.getDeclaredField("theUnsafe"); //设置为可存取 field.setAccessible(true); //获取该变量的值 unsafe = (Unsafe)field.get(null); //获取state在TestUnSafe中的偏移量 stateOffset = unSafe.objectFieldOffset (TestUnsafe.class.getDeclaredField("state")); }catch(Exception ex){ System.out.print(ex.getLocalizedMassage()); throw new Error(ex); } } public static void main(String[] args){ //创建实例,并且设置state值为1 TestUnSafe test = new TestUnsafe(); //6 Boolean sucess = unsafe.compareAndSwapInt(test,stateOffset,0,1); System.out.println(sucess); } }
9. Java指令重排序
多线程下指令重排会存在问题。
写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
10. 伪共享
- 两级cache结构
在Cache内部是按行存储的,其中每一行称为一个Cache行。Cache行(如图2-7所示)是Cache与主内存进行数据交换的单位,Cache行的大小一般为2的幂次数字节。
- 什么是伪共享
当CPU访问某个变量时,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。由于存放到Cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。
-
为何会出现伪共享
因为放入缓存中的数据是按缓存行的大小,若变量地址连续,则一次缓存放入可能会将多个变量同时放入一个缓存行。
-
如何避免伪共享
JDK 8之前可以通过字节填充的方式来避免该问题。
JDK 8提供了一个sun.misc.Contended注解,用来解决伪共享问题。将上面代码修改为如下。
@sun.misc.Contented public final static class FilledLong{ public volatile long value = 0L; }
11. 锁的概述
-
乐观锁与悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。
-
公平锁与非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在运行时闯入,也就是先来不一定先得。
ReentrantLock提供了公平和非公平锁的实现。
公平锁:ReentrantLock pairLock = new ReentrantLock(true)。
非公平锁:ReentrantLock pairLock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是非公平锁。
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
-
独占锁与共享锁
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。
独占锁是一种悲观锁,保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则是一种乐观锁,可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。 -
可重入锁
当一个线程再次获取它自己已经获取的锁时如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数地进入被该锁锁住的代码。
-
自旋锁
自旋锁是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。

浙公网安备 33010602011771号