并发编程中的三个问题:
可见性(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")@Statepublic 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操作,将会清空工作内存中此变量的值
- 对一个变量执行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变量)会记录线程获得几次锁。
可重入的好处:
可以避免死锁
可以让我们更好的来封装代码
小结:
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的所有权。其过程如下:
若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
monitorenter小结:
synchronized的锁对象会关联一个monitor, 这个monitor不是我们主动创建的, 是JVM的线程执行到这个同步代码块,发现锁对象
有monitor就会创建monitor, monitor内部有两个重要的成员变量owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,
当一个线程拥有monitor后其他线程只能等待。
monitorexit:
能执行monitorexit 指令的线程一定是拥有当前对象的monitor的所有权的线程。
执行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原子操作的性能消耗,不然就得不偿失了。
偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
- 虚拟机将会把对象头中的标志位设为"01",即偏向模式。
- 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
偏向锁的撤销
偏向锁的撤销动作必须等待全局安全点
暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态
偏向锁是自适应的
小结:
偏向锁的原理是什么?
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为"01",即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的MarkWord之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
偏向锁的好处是什么?
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
轻量级锁
什么是轻量级锁?
轻量级锁是JDK 6之中加入的新型锁机制,轻量级锁并不是用来代替重量级锁的。
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁,所以轻量级锁的出现并非是要代替重量级锁。
轻量级锁原理:
当关闭偏向锁或多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
- 判断当前对象是否处于无锁状态,如果是,则JVM 首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝,将对象的Mark Word 复制到栈帧中的Lock Record 中,将Lock Record中的owner指向当前对象。
- JVM 利用CAS 操作尝试将对象的Mark Word 更新为指向Lock Record 的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
- 如果失败则判断当前对象的Mark Word 是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
轻量级锁的释放:
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
- 取出在获取轻量级锁时保存在Mark Word 中的数据;
- 用CAS 操作将取出的数据替换当前对象的Mark Word 中,如果成功,则说明释放锁成功。
- 如果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
原文转载:http://www.shaoqun.com/a/492898.html
一淘网比价平台:https://www.ikjzd.com/w/1698
olive:https://www.ikjzd.com/w/2025
synchronized并发编程中的三个问题:可见性(Visibility)是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。代码演示:publicclassTest01Visibility{publicstaticbooleanflag=true;publicstaticvoidmain(String[]args)throwsInterruptedException{newThre
拍拍:拍拍
zozotown:zozotown
去海南亚龙湾旅游怎样乘车:去海南亚龙湾旅游怎样乘车
Adplexity ecommerce:Adplexity ecommerce
Amazon Influencer Program:Amazon Influencer Program
No comments:
Post a Comment