线程的同步就是保证多个线程的共同资源在同一时刻只有一个线程在使用和修改,保证数据是唯一的和准确的。
1.线程间的同步方式
1.1 互斥量
采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。例如,Java 中的 synchronized 关键词和各种 Lock 都是采用互斥对象机制。
1.2 信号量
信号量允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
1.3 事件
事件机制通过通知的方式保持多线程同步,还可以实现多线程优先级。
2.Java的线程同步
① 线程同步的目的是保证数据的唯一性和正确性。
Java的内存模型JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
Java的内存模型 JMM② 线程同步的主要解决原子性、可见性和有序性。
A.原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。
B.可见性 :当一个线程修改了共享变量,那么其余线程都能立即可以看到修改后的最新数据。volatile 关键字可以保证共享变量的可见性。
C.有序性 :代码的执行是有先后顺序的。然而 Java 在编译器以及运行期间的优化,导致代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
2.1 volatile
Java语言提供volatile关键字,用于修饰变量,在多线程并发中保证了共享变量的“ 可见性”。
volatile保证变量的可见性volatile关键字的作用:
① 保证变量的可见性,即告诉Java内存模型,volatile修饰的变量是不稳定的,每次使用都要到主内存中读取。
② 防止指令重排序。
2.2 synchronized
synchronized 关键字底层原理属于 JVM 层面。
(1)synchronized 修饰代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
SynchronizedDemo.classsynchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
(2)synchronized 修饰方法
SynchronizedDemo2.classpublic class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
参考:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md
2.3 CAS
CAS在Java并发应用中通常指CompareAndSwap或CompareAndSet操作,即比较并交换。
① CAS是一个原子操作,比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。
② JVM中的CAS操作是利用了 CPU 的CMPXCHG指令实现的。
Java 提供了 CAS 的原子类:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等。使用样例如下:
public class CASTest{
static AtomicInteger i = new AtomicInteger(0);
public static void increment() {
// 自增 1并返回之后的结果
i.incrementAndGet();
}
}
问题1:ABA问题
当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。这就是ABA问题。 为了解决 ABA 问题。Java 中提供了 AtomicStampedReference 类,可以进行版本控制。
问题2:CAS 没有加锁导致所有的线程都可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i 的值老是被修改了,所以线程又到回到第一步继续重头再来。如果线程密集,就会循环消耗资源。
Java8 引入了一个 cell[] 数组,它的工作机制是这样的:假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,等价于 100 个线程对 i 进行了 100 次自增操作。
网友评论