线程同步synchronized

六眼飞鱼酱① 提交于 2020-03-17 11:41:01

同步条件:等待池序列,锁机制。

       由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的我程挂起;
  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题:
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

可以修饰方法或者代码块,确保多个线程在同一时刻,只能有一个线程处理方法或者是同步块,Synchronized修饰的方法或者代码块相当于并发中的临界区,在同一时刻JVM只允许一个线程进入执行。保证线程对访问变量的可见性,有序性,原子性

  • 修饰普通方法
  • 修饰静态方法
  • 修饰代码块

通过synchronized关键字来处理统计1秒钟count++的次数

public class SynchronizedDemo {
    private  static boolean flag = true;
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (isTrue()) {
                    count++;
                }
                System.out.println("count :" + count);
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //1秒钟后将标志位修改为false
                SynchronizedDemo.setFlag(false);
                System.out.println("1秒钟结束");
            }
        });
        thread.start();
        thread1.start();
    }

    private static synchronized boolean isTrue(){
        return SynchronizedDemo.flag;
    }

    private static synchronized void setFlag( boolean falg) {
        SynchronizedDemo.flag = falg;
    }
}

Synchronized的原理

Synchronized修饰的代码块和方法,通过javap反编译字节码文件可知,同步代码块中使用到了monitorentermonitorexit执行,同步方法上在flags增加了ACC_SYNCHRONIZED修饰符。

两种方式,无论哪一种,本质上是获取对象的监视器(Monitor),对象监视器的获取是排他的,同一时刻只能有一个线程来获取到Synchronized所保护对象的监视器。

synchronized允许使用任何的一个对象作为同步的内容,因此任意一个对象都应该拥有自己的监视器(monitor),当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

监视器monitor的获取是需要借助OS系统提高的同步机制(Mutex Lock)来处理的,来获取Synchronized锁即monitor时候需要用户空闲(Java线程)和内核空间尽进行切换,用户空间和内核空间的切换需要耗时,因此Synchronize是重量级锁。

 

Synchronized的优化

在JDK1.5之前,只有上面介绍的Synchronized重量级锁,实现需要借助操作系统,是比较消耗性能的操作,在JDK1.5之后,对Synchronized进行优化,优化包括偏向锁,轻量级锁,重量级锁。

Java对象内存布局

 

 

  • 对象头区域    存放锁信息,对象年龄等信息
  • 实例区域    存储的是对象真正有效的信息
  • 填充区域    JVM中规定数据的读取字节是8字节的整数倍,一次性读取8字节的整数倍数据,数据不足,就需要补位对齐,补位空间就是填充区域,填充区域大小是不固定。

 

锁有四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着线程竞争情况逐渐升级、锁可以升级但是不可以降级,意味着偏向锁升级轻量级锁不能降回到偏向锁,轻量级锁到重量级锁是一样,目的是提高获得锁和释放锁的效率

偏向锁

       偏向锁的操作不需要操作系统的介入,每个对象的对象头中的Mark Word的表示来区分当前的锁。

    JVM使用CAS操作将线程ID的记录到Mark Word当中,修改标识位,当前线程就可以获取到锁当线程来获取锁,执行Synchronized修饰的方法或者代码块,第一次线程获取操作将线程Id记录到对象头中,当再次来时,JVM通过对象头的Mark Work判断(当前的线程ID,当前的线程持有的对象的锁,继续来获取当前的对象),这个就是偏向锁,在线程没有竞争的时候,一直都是一个线程来执行。

轻量级锁

      轻量级锁也不需要操作系统的介入,JVM对偏向锁做一下升级,变成一个轻量级锁。JVM把锁对象Account恢复成无锁状态,在当前的线程(来竞争当前对象的线程)的堆栈中个分配一个空间,叫做Lock Record空间,把锁对象的Mark Work的堆中各复制一份,叫做Displaced Mark Word,当某个线程抢到锁,将当前线程的Lock Record的地址使用CAS操作放回到Mark Word当中,并且将锁的标志修改为00,意味着当前的线程获取到了轻量级锁。

其他未获得锁的线程,不会阻塞(当前还是会持有COU的执行权),JVM会让线程进行自旋几次,等待获取锁的线程释放锁,需要将需要把这个Displaced markd word 使用CAS复制回去,接下来其他的线程就可以来后去锁,线程进行交流获取当前的锁,执行代码。这里存在着轻度的竞争,轻量级锁仅仅使用CAS操作和Lock record就避免了重量级锁的开销,轻量级锁存在少量的线程的竞争。

 重量级锁

       轻量级锁的运行时,未获取到锁的线程自旋很多次都无法获取锁,考虑自旋次数太多浪费CPU,升级为重量级锁重量系统的接入,依赖操作系统的Mutex Lock,JVM 创建一个monitor对象,将对象的地址更新到Mark Word当中。如果未获取到锁的线程,切换到阻塞状态。

总结:

JVM设置偏向锁和轻量级锁,就是为了避免阻塞,避免操作系统的介入, 这两种锁无非就是针对这两种情况:

偏向锁:通常只有一个线程在临界区执行。

轻量级锁: 可以有多个线程交替进入临界区,在竞争不激烈的时候,稍微自旋等待一下就能获得锁。

重量级锁:需要操作系统介入,那就是出现了激烈的竞争,未获取到锁的线程进入阻塞状态。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!