Synchronized的作用:
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则该对象变量的所有读取或写入都是通过同步方法完成的.
一句话说出Synchronized的作用
能够保证同一时刻最多只有一个线程执行该段代码,以保证并发安全的效果
Synchronized的地位
1.Synchronized是Java的关键字,被Java语言原生支持
2.是最基本的互斥同步手段
3.是并发编程中的元老,必学
不使用并发手段会有什么后果?
两个线程同时a++,最后会比预期的少,而且每次执行的结果都不一样
public class DisappearRequest implements Runnable{ static DisappearRequest1 instance = new DisappearRequest1(); static int i = 0; public static void main(String[] args)throws Exception { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } @Override public void run() { for (int j = 0; j < 1000000; j++){ i++; } } }
原因:
count++,它看上去是一个操作,实际上包含了三个动作这三个动作如果不按照原子去执行就会产生错误:
1.读取Count
2.将Count+1
3.将Count的值写入到内存中
假设count是9,线程A进入到这个方法中读到count是9把它加1,但是还没来得及写进内存中B线程也来了读取这个方法它读到的还是9,它也加1.也变成10,他们都只会把是10写入到内存中,然后就会发生两个线程都加1了但是最终写入到内存中却是10的情况.
这就是线程不安全!
Synchronized的两个用法
对象锁
包含方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己制定锁对象)
代码块形式:手动制定锁对象,在代码块上加上synchronized关键字
方法锁的形式:使用synchronized关键字去修饰普通方法,锁对象默认this
this锁对象只有一把锁,需要前面的线程释放第二个线程才能拿到所进入方法它是串行,自己制定锁对象可以并行
类锁
指synchronized修饰静态方法或制定锁对象为Class对象
1.概念:Java类可能有很多歌对象,但只有一个Class对象,所以不管是从哪个实例过来的它都只有一把锁,类锁是一个概念上的东西并不是说它真实存在,它这个概念是用来帮我们理解实例方法和静态方法的区别的.所谓的类锁,不过是Class对象的锁而已,因为class对象只有一个
类锁的效果是只能在同一时刻被一个对象拥有,即使是不同的Runnable实例所对应的类所依然只有一个,这点跟对象锁是不同的,对象锁如果是不同的实例创建出来互相是不影响的,它们可以同时运行,但是只要用了类锁就只有一个能运行了.
2.形式1:synchronized加在static方法上
3.形式2:synchronized加在(*.class)代码块上
多线程访问同步方法的7中情况
1.两个线程同时访问一个对象的同步方法
这里它们是同一把锁(this),争抢同一把锁的时候必然要相互等待只能有一个人持有
2.两个线程访问的是两个对象(实例)的同步方法
这种情况下synchronized是不起作用的,它们之间是不受干扰的原因是它们的锁对象不是同一个,所以不干扰并行执行
3.两个线程访问的是synchronized的静态方法
虽然它们是不同的实例,但是只要它们是静态的,那么对应的锁就是同一把,它们会一个一个执行.
4.同时访问同步方法与非同步方法(被synchronized修饰和没有被synchronized修饰的方法)
同时访问同步方法与非同步方法,synchronized只作用与加了同步的方法中,没有加synchronized的方法不会受到影响
5.访问同一个对象的不同的普通同步方法(不是static非静态的方法,那么它们是串行还是并行)
这两个都加了Synchronized的方法是同一个实例拿到的都是一样this,所以这两个方法没有办法同时运行,它们是串行执行的
6.同时访问静态synchronized和非静态synchronized方法(都是被synchronized所修饰的,不同的
是一个是静态的一个不是静态的,这个时候有多线程去并发执行,会带来怎样的同步效果)
静态synchronized修饰的方法是类锁,非静态synchronized方法是方法锁形式,一个是.class一个是对象实例的this虽然都加了Synchronized但是它们还是会同时执行同时结束
7.如果方法抛出异常后,会释放锁吗?
方法抛出异常之后JVM会帮它释放锁,其它方法就会获得这把锁接着往下执行
(跟lock接口比较,lock即便抛出异常,你没有显式的去释放锁,lock是不会释放的,synchronized一旦抛出异常它会自动的释放的)
7种情况总结:
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况);
2.每个实例都对应有自己的一把锁,不同的实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有的对象共用同一把类锁(对应第2,3,4,6种情况);
3.无论是方法正常执行完毕或者是方法抛出异常,都会释放锁(对应第七种情况)
4.被synchronized修饰的方法调用了另个一个没有被synchronized修饰的方法,还是线程安全吗?
不是,一旦出了本方法而另一个方法没有被synchronized修饰那它是可以同时被多个线程访问的
synchronized的性质:
1:可重入
什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
好处:避免死锁,提升封装性
可重入的粒度:线程而非调用(用3中情况来说明和pthread的区别)
同一个方法是可重入的
不同的方法也是可重入的
不同的类一样符合可重入的标准
可重用的粒度是线程范围而不是调用范围的,就是说在同一个线程中它已经拿到了一把锁而且还想接着使用这把锁去访问其它的方法或者其他类的方法只要需要的还是这把锁它就不需要去释放锁可以接着使用
2.不可中断
一旦这个锁被别获得了,如果我还想获得,那么我就只能选择等待或者阻塞,直到别的线程释放这个锁.如果别人永远不释放锁,由于我拥有不可中断的性质那么我只能永远的等待下去.
(相比Lock锁,它可以拥有中断的能力,第一点,如果它觉得等待的时间太长了,有权中断现在已经获得到锁的线程的执行;第二点,如果它觉得它等待的时间太长了不想再等了,也可以选择退出)
Synchronized原理:
1.加锁和释放锁的原理:
1现象:
每一个类的实例对应一把锁,每一个synchronized方法都必须先获得调用该方法的实例的锁才能执行,否则线程会进入阻塞,而方法一旦执行了就会独占这把锁,只有该方法返回或者是抛出异常才会释放,只有它释放了锁其它线程才能进入运行的状态,所有的java对象都含有一个互斥锁,这个锁由jvm自动的去获取和释放
2.获取和释放锁的时机:
内置锁(监视器锁),线程在进入到同步代码块前会自动获取这把锁并且在退出这把锁之后会自动的释放,获得这个内置锁的唯一途径就是进入到synchronized锁保护的同步代码块或方法中.
3.JVM字节码(反编译/monitor)
public class Decompilation14 { private Object object = new Object(); public void insert(Thread thread){ synchronized (object){ } } }
javap -verbose
1 F:Javaresourcecodesynchronizedsrc>javac Decompilation.java 2 3 F:Javaresourcecodesynchronizedsrc>javap -verbose Decompilation.class 4 Classfile /F:/Javaresource/code/synchronized/src/Decompilation.class 5 Last modified 2020-9-14; size 479 bytes 6 MD5 checksum 5a99c0bb18811ba93a00794292860597 7 Compiled from "Decompilation14.java" 8 public class Decompilation14 9 minor version: 0 10 major version: 52 11 flags: ACC_PUBLIC, ACC_SUPER 12 Constant pool: 13 #1 = Methodref #2.#20 // java/lang/Object."<init>":()V 14 #2 = Class #21 // java/lang/Object 15 #3 = Fieldref #4.#22 // Decompilation14.object:Ljava/lang/Object; 16 #4 = Class #23 // Decompilation14 17 #5 = Utf8 object 18 #6 = Utf8 Ljava/lang/Object; 19 #7 = Utf8 <init> 20 #8 = Utf8 ()V 21 #9 = Utf8 Code 22 #10 = Utf8 LineNumberTable 23 #11 = Utf8 insert 24 #12 = Utf8 (Ljava/lang/Thread;)V 25 #13 = Utf8 StackMapTable 26 #14 = Class #23 // Decompilation14 27 #15 = Class #24 // java/lang/Thread 28 #16 = Class #21 // java/lang/Object 29 #17 = Class #25 // java/lang/Throwable 30 #18 = Utf8 SourceFile 31 #19 = Utf8 Decompilation14.java 32 #20 = NameAndType #7:#8 // "<init>":()V 33 #21 = Utf8 java/lang/Object 34 #22 = NameAndType #5:#6 // object:Ljava/lang/Object; 35 #23 = Utf8 Decompilation14 36 #24 = Utf8 java/lang/Thread 37 #25 = Utf8 java/lang/Throwable 38 { 39 public Decompilation14(); 40 descriptor: ()V 41 flags: ACC_PUBLIC 42 Code: 43 stack=3, locals=1, args_size=1 44 0: aload_0 45 1: invokespecial #1 // Method java/lang/Object."<init>":()V 46 4: aload_0 47 5: new #2 // class java/lang/Object 48 8: dup 49 9: invokespecial #1 // Method java/lang/Object."<init>":()V 50 12: putfield #3 // Field object:Ljava/lang/Object; 51 15: return 52 LineNumberTable: 53 line 1: 0 54 line 2: 4 55 56 public void insert(java.lang.Thread); 57 descriptor: (Ljava/lang/Thread;)V 58 flags: ACC_PUBLIC 59 Code: 60 stack=2, locals=4, args_size=2 61 0: aload_0 62 1: getfield #3 // Field object:Ljava/lang/Object; 63 4: dup 64 5: astore_2 65 6: monitorenter 66 7: aload_2 67 8: monitorexit 68 9: goto 17 69 12: astore_3 70 13: aload_2 71 14: monitorexit 72 15: aload_3 73 16: athrow 74 17: return 75 Exception table: 76 from to target type 77 7 9 12 any 78 12 15 12 any 79 LineNumberTable: 80 line 4: 0 81 line 5: 7 82 line 6: 17 83 StackMapTable: number_of_entries = 2 84 frame_type = 255 /* full_frame */ 85 offset_delta = 12 86 locals = [ class Decompilation14, class java/lang/Thread, class java/lang/Object ] 87 stack = [ class java/lang/Throwable ] 88 frame_type = 250 /* chop */ 89 offset_delta = 4 90 } 91 SourceFile: "Decompilation14.java"
1.monitorenter:
会让锁对象+1,monitor计数器为0就会让线程立刻获得计数器+1,加1之后别的线程就会看到这把锁已经被别所获得了,如果说monitor已经拿到了可重入的情况下回累加+1变成2.如果monitor已经被其它线程所持有了,那我去获取它的时候就会得到已经被其它线程获取了,我这个时候就会进入阻塞状态,直到monitor计数器变为0,我才会再次去获取锁
2.monitorexit:
释放锁的所有权,就是将monitor的计数器减一,如果变成0了那么我就不在持有对monitor的持有权了,减完之后不是0,那意味着我刚才是可重入进来的,那么我还继续持有这把锁,当我变成0 的时候意味着我已经释放锁了,还意味着其它被阻塞的线程会再次会再次尝试获取对该锁的所有权
2.可重入原理:加锁次数计数器
1.JVM负责跟踪对象被加锁的次数
2.线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程再次对象上再次获得锁时,计数会递增
3.每当任务离开时,计数递减,当计数为0的时候,锁会被完全释放
3.保证可见性原理:内存模型
两个线程想要通信的话,它们是如何做到的
线程A下面有本地内存A,本地内存A和本地内存B里面存的变量都是一个副本,它们都是把主内存中的变量都复制一份放到本地内存,这样的作用是可以加速程序的运行,因为线程运行的速度往往要比主程序快
两个线程想要通信要经过以下步骤:
1.线程A要把副本写到主内存中去,因为主内存是它们相互沟通的桥梁
2.线程A把副本写入到内存中之后线程B再去读取它就可以读取到线程A更新的变量了(这一步是通过JMM来实现的)
synchronized是如何做到可见性的实现的,一旦被synchronized关键字锁修饰那么在执行完毕之后被锁住的对象所做的任何修改都要在释放锁之前,从线程内存写回到主内存中,它不会存在线程内存和主内存不一致的情况,它在被synchronized锁住之后它的数据也是从主内存中读取出来的,这时候从主内存中读到的数据一定是最新的.
synchronized的缺陷
1.效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
2.不够灵活(读写锁更灵活:读不加,写才加):加锁和释放锁的时机单一,每个锁仅有单一的添加(某个对象),可能是不够的
3.无法知道是否成功获取到锁(lock可以去尝试成功了做一些逻辑业务,没成功做另一些逻辑)
常见问题:
1.使用注意点:锁对象不能为空,作用域不宜过大,避免死锁
2.如何选择Lock和Synchronized关键字?
如何选择Lock和synchronized关键字
1)如果可以的话,两者都不要使用,使用JUC中的各种类
2)如果synchronized关键字,在程序中适用,那么就优先使用(可以减少所编写的代码)
3)如果特别需要使用到Lock独有的特性时,就是用
建议思路:避免出错,有现成的工具包就直接使用。没有的话,优先使用synchronized(减少代码编写)。如果需要灵活的加锁、释放锁就使用Lock
3.多线程访问同步的各种具体情况
思考:
1、多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
1)内部锁调度机制
2)线程释放锁之后,竞争锁的对象有:等待中的线程、刚刚申请这把锁的线程
3)实现细节和JVM的版本、具体实现相关,不能依赖算法
2、synchronized使得同时只有一个线程可以执行,性能较差,有什么方法可以提升性能?
1)优化使用范围
2)使用其他类型的锁(读写锁)
3、想灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
1)自己实现一个锁
4、什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
1)所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。