关键字:synchronized

保证三大特性

原子性

synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发编程的效果。

没有synchronized:

public class AtomicTest {
    private static int v = 0;
    public static void main(String[] args) {
        new Thread(AtomicTest::add).start();
        new Thread(AtomicTest::add).start();
        new Thread(AtomicTest::add).start();
        new Thread(AtomicTest::add).start();
        new Thread(AtomicTest::add).start();
        while (Thread.activeCount()>2){

        }
        System.out.println(v);
    }
    private static void add(){
        for (int i = 0; i < 10000; i++) {
            v++;
        }
    }
}

image-20210122092408586

给add方法加上synchronized后,每次执行结果就是50000了

    private static final Object obj = new Object();
    private static void add(){
        synchronized (obj){
            for (int i = 0; i < 10000; i++) {
                v++;
            }
        }
    }

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

可见性

没有synchronized:

    private static boolean flag = true;
    public static void main(String[] args) {
        new Thread(()->{
            while (flag){
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()-> flag = false).start();
    }

while循环不停止

加上synchronized:

    private static boolean flag = true;
    private static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            while (flag){
                synchronized (obj){

                }
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()-> flag = false).start();
    }

运行2秒后停止,因为synchronized对应的lock原子操作,会刷新线程工作内存中的共享变量在主内存中的最新值。

有序性

@JCStressTest
@Outcome(id={"1","4"},expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id="0",expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
@State
public class OrderTest {
    int num = 0;
    boolean ready = false;

    @Actor
    public void actor1(I_Result r){
        if(ready){
            r.r1 = num+num;
        }else{
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r){
        num = 2;
        ready = true;
    }
}

image-20210122102934001

出现返回结果是0的情况只有一种,先执行了actor2中的ready=true,然后actor1继续执行,这时候num=0,所以返回值为0+0等于0。证明了发生重排序

添加synchronized:

    private final Object obj = new Object();    
    @Actor
    public void actor1(I_Result r){
        synchronized (obj){
            if(ready){
                r.r1 = num+num;
            }else{
                r.r1 = 1;
            }
        }
    }

    @Actor
    public void actor2(I_Result r){
        synchronized (obj){
            num = 2;
            ready = true;
        }
    }

image-20210122103835777

synchronized保证有序性原理,我们加上synchronized后,依然会发生重排序,但是我们有同步代码块,以保证只有一个持有锁对象的线程执行同步代码块中的代码,保证有序性。

synchronized的特性

可重入特性

含义:一个线程可以多次执行synchronized,重复获取同一把锁

public class ReentryTest {
    public static void main(String[] args) {
        new ReentryTestThread().start();
        new ReentryTestThread().start();
    }
}

class ReentryTestThread extends Thread{

    @Override
    public void run() {
        synchronized (ReentryTestThread.class){
            System.out.println(Thread.currentThread().getName()+"------1");
            synchronized (ReentryTestThread.class){
                System.out.println(Thread.currentThread().getName()+"------2");
            }
        }
    }
}

image-20210122105055877

可重入原理:synchronized的锁对象中有一个计数器(recursions)变量会记录线程获得几次锁。在执行完同步代码块时,计数器的数量会-1,直到计数器数量为0时,就释放锁。

可重入好处:

  • 避免死锁
  • 可以让我们更好的来封装代码

不可中断特性

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

    private static Object OBJECT = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable run= () -> {
            synchronized (OBJECT) {
                System.out.println("进入同步代码块");
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread1 = new Thread(run);
        thread1.start();
        Thread.sleep(1000);
        Thread thread2 = new Thread(run);
        thread2.start();
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());
        thread2.interrupt();
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());
    }

image-20210122112416159

反汇编学习synchronized原理

public class Demo1 {

    private static Object obj = new Object();

    public static void main(String[] args) {
        synchronized (obj){
            System.out.println("1");
        }
    }

    public synchronized void test(){
        System.out.println("a");
    }
}
javap -p -v Demo1.class

main方法中:

image-20210122135446262

monitorenter

jvm规范中对于monitorenter描述:

image-20210122140113491

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

objectref必须是reference类型。

每个对象都与一个监视器相关联。当且仅当监视器有所有者时才锁定监视器。执行monitorenter的线程尝试获取与当前对象关联的监视器的所有权,如下所示:

如果与关联的monitor的条目计数为零,则线程进入监视器并将其条目计数设置为1。然后线程就是监视器的所有者。

如果线程已经拥有与objectref关联的monitor,它将重新进入监视器,并增加其条目计数。

如果另一个线程已经拥有与objectref关联的monitor,则该线程将阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。

所以monitor才是真正的锁,JVM会创建一个monitor C++对象。

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是jvm的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个中重要的成员变量owner,拥有这把锁的线程,recursions会记录线程拥有锁的次数,一个线程拥有monitor后其他线程只能等待。

monitorexit

image-20210122141210143

objectref必须是reference类型。
执行monitorexit的线程必须是与objectref引用的实例关联的监视器的所有者。
线程减少与objectref关联的监视器的条目计数。如果结果是entry count的值为零,则线程将退出监视器,不再是其所有者。其他阻止进入监视器的线程可以尝试这样做。

总结

image-20210122141903205

注意:monitorexit出现了两次,为什么?

注意下面的exception table 中的 from to target:指的是从6到16中如果出现异常,直接跳转到19号指令。意思是之间如果出现异常了,没有正常monitorexit(正常释放锁),还有一个保底方案,22号指令也是monitorexit释放锁。

所以:synchronized代码块中出现异常,也会释放锁。

面试题:synchronized和Lock区别

  • synchronized是关键字,Lock是接口
  • synchronized会自动释放锁,Lock需要手动释放
  • synchronized是不可中断的,Lock可以中断,也可以不中断
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块
  • Lock可以使用读锁提高多线程读效率
  • synchronized是非公平锁,ReetrantLock可以控制是否是公平锁

synchronized优化

Java对象的布局

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

image-20210124170456728

对象头:当前一个线程尝试访问synchronized修饰的代码块时,他首先要获得锁,这个锁是存在锁对象头中的

偏向锁

偏向锁是jdk1.6的中澳引进,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

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

image-20210125103931127

偏向锁的原理

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

偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。

偏向锁好处

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

轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

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

轻量级锁加锁过程

  • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word

image-20210125110819133

  • 拷贝对象头中的Mark Word复制到锁记录中;
  • 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

image-20210125110851861

  • 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁

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

自旋锁

前面我们讨论 monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) , 这项技术就是所谓的自旋锁。

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

适应性自旋锁

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

平时写代码如何对synchronized优化

减少synchronized范围

同步代码块中代码尽量短小精悍

降低锁的粒度

将一个锁拆分为多个锁提高并发度

HashTable:锁定整个hash表,一个操作正在执行时,其他操作也同时锁定,效率低下

image-20210125115443111

ConcurrentHashMap:局部锁定,只锁定桶,当对当前元素锁定时,其他元素不锁定。

image-20210125115422888

LinkedBlockingQueue:入队出队使用不同的锁,相对于读写只有一把锁效率要高。

image-20210125115530655

读写分离

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

ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteSet

原文地址:https://www.cnblogs.com/wwjj4811/p/14324497.html