synchronized

并发编程中的三个问题:

可见性(Visibility)

是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。

代码演示:

public class Test01Visibility {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) { }
}).start(); Thread.sleep(2000); new Thread(() -> {
flag = false;
System.out.println("修改了flag");
}).start();
}
}

小结:并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改

后的最新值。

原子性(Atomicity)

在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行

代码演示:

public class Test02Atomicity {
public static int num = 0;
public static void main(String[] args) throws InterruptedException {
// 创建任务
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
num++;
}
};
ArrayList<Thread> threads = new ArrayList<>();
//创建线程
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
threads.add(t);
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
}

通过 javap -p -v Test02Atomicity对class 文件进行反汇编:发现++ 操作是由4条字节码指令组成,并不是原子操作

小结:并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作

有序性(Ordering)

是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

代码演示:

@JCStressTest
@Outcome(id={"1","4"},expect=Expect.ACCEPTABLE,desc="ok")
@Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger")
@State
public class Test03Orderliness {
int num=0;
boolean ready=false;
//线程一执行的代码
@Actor
public void actor1(I_Resultr){
if(ready){
r.r1=num+num;
}else{
r.r1=1;
}
}
//线程2执行的代码
@Actor
public void actor2(I_Resultr){
num=2;
ready=true;
}
}

运行的结果有:0、1、4

小结:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必

就是开发者编写代码时的顺序。

Java内存模型(JMM)

计算机结构简介

根据冯诺依曼体系结构,计算机由五大组成部分,输入设备,输出设备,存储器,控制器,运算器。

CPU:

中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中的数据。

内存:

我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。

缓存:

CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。于是就有了在

CPU和主内存之间增加缓存的设计。CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。

Java内存模型

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

工作内存

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

小结

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

主内存与工作内存之间的交互

注意:1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值

  1. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

synchronized保证三大特性

synchronized保证可见性

while(flag){
//增加对象共享数据的打印,println是同步方法
System.out.println("run="+run);
}

小结:

synchronized保证可见性的原理,执行synchronized时,lock原子操作会刷新工作内存中共享变量的值。

synchronized保证原子性

for(int i = 0; i < 1000; i++){
synchronized(Test01Atomicity.class){
number++;
}
}

小结:

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

synchronized保证有序性

synchronized(Test01Atomicity.class){
num=2;
ready=true;
}

小结

synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性。

synchronized的特性

可重入特性

public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(Thread.currentThread().getName() + "获取了锁1");
synchronized (MyThread.class) {
System.out.println(Thread.currentThread().getName() + "获取了锁2");
}
}
}
}

可重入原理:

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。

可重入的好处:

  1. 可以避免死锁

  2. 可以让我们更好的来封装代码

小结:

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,获取一次锁加+1,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

不可中断特性

什么是不可中断?

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

public class Uninterruptible {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable run = () -> {
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "执行同步代码块");
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}; Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start(); Thread.sleep(1000);
System.out.println("停止线程2前");
System.out.println(t2.getState());
t2.interrupt();
System.out.println("停止线程2后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}

synchronized是不可中断,处于阻塞状态的线程会一直等待锁。

ReentrantLock可中断演示

public class Interruptible {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
} private static void test01() throws InterruptedException {
Runnable run = () -> {
boolean flag = false;
String name = Thread.currentThread().getName();
try {
flag = lock.tryLock(3, TimeUnit.SECONDS);
if (flag) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(888888);
} else {
System.out.println(name + "没有获得锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (flag) {
lock.unlock();
System.out.println(name + "释放锁");
}
}
}; Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000); Thread t2 = new Thread(run);
t2.start();
}
}

小结:

synchronized属于不可被中断

Lock的lock方法是不可中断的

Lock的tryLock方法是可中断的

synchronized 的原理

monitorenter:

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)

  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1

  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorenter小结:

synchronized的锁对象会关联一个monitor, 这个monitor不是我们主动创建的, 是JVM的线程执行到这个同步代码块,发现锁对象

有monitor就会创建monitor, monitor内部有两个重要的成员变量owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,

当一个线程拥有monitor后其他线程只能等待。

monitorexit:

  1. 能执行monitorexit 指令的线程一定是拥有当前对象的monitor的所有权的线程。

  2. 执行monitorexit 时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

monitorexit释放锁。

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

面试题synchroznied出现异常会释放锁吗?

:会释放锁。

同步方法

同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰。会隐式调用monitorenter 和monitorexit。在执行同步方法前会调用

monitorenter,在执行完同步方法后会调用monitorexit 。

小结:

通过javap反汇编可以看到synchronized 使用了monitorentor和monitorexit两个指令。每个锁对象都会关联一个monitor(监视

器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数, 当执行到

monitorexit时, recursions会-1, 当计数器减到0时这个线程就会释放锁。

面试题:synchronized与Lock的区别

1、synchronized 是关键字,lock 是一个接口

2、synchronized 会自动释放锁,lock 需要手动释放锁。

3、synchronized 是不可中断的,lock 可以中断也可以不中断。

4、通过lock 可以知道线程有没有拿到锁,而synchronized 不能。

5、synchronized 能锁住方法和代码块,而lock 只能锁住代码块。

6、lock 可以使用读锁提高多线程读效率。

7、synchronized 是非公平锁,ReentrantLock 可以控制是否是公平锁。

CAS

cas的概述和作用:

compare and swap,可以将比较和交换转为原子操作,这个原子操作直接由cpu保证,cas可以保证共享变量赋值时的原子操作,cas依赖3个值:内存中的值v,旧的预估值x,要修改的新值b。根据atomicInteger的地址加上偏移量offset的值可以得到内存中的值,将内存中的值和旧的预估值进行比较,如果相同,就将新值保存到内存中。不相同就进行重试。

Java对象的布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc。

_mark表示对象标记、属于markOop类型,也就是Mark World,它记录了对象和锁有关的信息

_metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、compressed_klass表示压缩类指针。

Mark Word

锁状态 存储内容 锁标志位
无锁 对象的hashcode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程id、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

klass pointer

用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。通过-XX:+UseCompressedOops开启指针压缩,

在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

实例数据

就是类中定义的成员变量。

对齐填充

由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象的大小必须是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对齐填来补全。

查看Java对象布局

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

小结

Java对象由3部分组成,对象头,实例数据,对齐数据,对象头分成两部分:Mark World + Klass pointer

偏向锁

什么是偏向锁?

锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

偏向锁原理

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点

  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态

  3. 撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态

偏向锁是自适应的

小结:

偏向锁的原理是什么?

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的MarkWord之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的好处是什么?

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

轻量级锁

什么是轻量级锁?

轻量级锁是JDK 6之中加入的新型锁机制,轻量级锁并不是用来代替重量级锁的。

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁,所以轻量级锁的出现并非是要代替重量级锁。

轻量级锁原理

当关闭偏向锁或多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  1. 判断当前对象是否处于无锁状态,如果是,则JVM 首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝,将对象的Mark Word 复制到栈帧中的Lock Record 中,将Lock Record中的owner指向当前对象。
  2. JVM 利用CAS 操作尝试将对象的Mark Word 更新为指向Lock Record 的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word 是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

轻量级锁的释放:

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁时保存在Mark Word 中的数据;
  2. 用CAS 操作将取出的数据替换当前对象的Mark Word 中,如果成功,则说明释放锁成功。
  3. 如果CAS 操作替换失败,说明有其他线程获取该锁,则需要将轻量级锁膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS 操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁好处:

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

自旋锁

monitor 实现锁的时候, monitor 会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU 从用户态转为核心态,频繁的阻塞和唤醒对CPU 来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。同时,共享数据的锁定状态可能只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果有一个以上的处理器,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍微等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否释放了锁。为了让线程等待,我们只需让线程执行一个循环(即自旋),这就是自旋锁。

自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改。

适应性自旋锁

在JDK 6 中引入了自适应的自旋锁。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。

平时写代码如何对synchronized优化

减少synchronized的范围:

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

synchronized(Demo01.class){
System.out.println("aaa");
}

降低synchronized锁的粒度:

将一个锁拆分为多个锁提高并发度,如HashTable:锁定整个哈希表,一个操作正在进行时,其他操作也同时锁定,效率低下。ConcurrentHashMap:局部锁定,只锁定桶。

读写分离:

读取时不加锁,写入和删除时加锁

ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

深入学习synchronized的更多相关文章

  1. Java再学习——synchronized与volatile

    volatile:只保证共享资源的可见性的,任何修改都写在主存,所有线程马上就能看到,适用于新值不依赖于旧值的情形. synchronized:保证可操作的原子性一致性和可见性. volatile和s ...

  2. Java多线程学习——synchronized锁机制

    Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...

  3. synchronized学习

    现代软件开发中并发已经成为一项基础能力,而Java精心设计的高效并发机制,正是构建大规模应用的基础之一.本文中我们将学习synchronized关键字的基本用法. synchronized是Java内 ...

  4. java线程之——synchronized的注意细节

    我在学习synchronized的时候,十分好奇当一个线程进入了一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? 然后就做了个实验(实验代码最后贴出),最后得到了如下 ...

  5. Java synchronized关键字用法(清晰易懂)

    本篇随笔主要介绍 java 中 synchronized 关键字常用法,主要有以下四个方面: 1.实例方法同步 2.静态方法同步 3.实例方法中同步块 4.静态方法中同步块 我觉得在学习synchro ...

  6. Java并发系列之Synchronized

    每一个刚接触多线程并发编程的同学,当被问到,如果多个线程同时访问一段代码,发生并发的时候,应该怎么处理? 我相信闪现在脑海中的第一个解决方案就是用synchronized,用锁,让这段代码同一时间只能 ...

  7. 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile

    章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...

  8. Java中的关键字synchronized

    1. 介绍 在Java并发系列的文章中,这个是第二篇文章.在前面的一篇文章中,我们学习了Java中的Executor池和Excutors的各种类别. 在这篇文章中,我们会学习synchronized关 ...

  9. Java程序员必精通之—synchronized

    更多Java并发文章:https://www.cnblogs.com/hello-shf/category/1619780.html 一.简介 相信每一个java程序员对synchronized都不会 ...

随机推荐

  1. js实现无缝连接轮播图(二)实现自定义属性,根据banner图片的数量动态生成小圆点

    <!-- 这个animate.js 必须写到 index.js的上面引入 --><script src="js/animate.js"></scrip ...

  2. cve-2020-1472,netlogon特权提升漏洞复现

    cve-2020-1472,netlogon特权提升漏洞, 漏洞原理:攻击者通过NetLogon(MS-NRPC),建立与域控间易受攻击的安全通道时,可利用此漏洞获取域管访问权限.成功利用此漏洞的攻击 ...

  3. 这是2020年最强Python学习路线,从入门到精通!

    给大家整理的这套python学习路线图,按照此教程一步步的学习来,肯定会对python有更深刻的认识.或许可以喜欢上python这个易学,精简,开源的语言.全民学Python的话题铺天盖地,中国的Py ...

  4. C语言之 Switch和?:运算符的反汇编

    Switch条件语句 通过上面一篇了解了条件语句的使用,接下来就直接进行反汇编学习 #include <stdio.h> void print() { int b = 1; switch ...

  5. Eureka整合sidecar异构调用

    本次使用nodejs脚本生成的异构程序测试: node-server.js var http = require('http'); var url = require('url'); var path ...

  6. TypeError: render() got an unexpected keyword argument ‘renderer‘

    关注公众号"轻松学编程"了解更多 原因 在使用xadmin时,如果是Django2版本以上的会报这个错误.这是由于xadmin中的文件xadmin\views\dashboard. ...

  7. python实现密码破解

    排列组合(破解密码) 关注公众号"轻松学编程"了解更多. 1.排列 itertools.permutations(iterable,n) 参数一:要排列的序列, 参数二:要选取的个 ...

  8. js音乐播放器【简洁】

    辞职的第二天没有去找工作还,准备回家. 但到了火车站才发现沃特玛的买的票不是在这个火车站坐. 这就耽误了行程...... 说出来真舒服!!!淦 代码 这里已经上传到码云了,大家可以直接引用. 目前只有 ...

  9. .netcore使用autofac

    .netcore3.1使用autofac (.netcore中本身已经实现了IOC容器,其实没有必要替换成autofac.如果非常习惯autofac,替换也是无可厚非的.) 第一步.在项目中引入Aut ...

  10. 【应用程序见解 Application Insights】Application Insights 使用 Application Maps 构建请求链路视图

    Applicaotn  Insigths 使用 Application Maps 构建请求链路视图 构建系统时,请求的逻辑操作大多数情况下都需要在不同的服务,或接口中完成整个请求链路.一个请求可以经历 ...