java锁学习

3 篇文章 0 订阅

Java并发编程

并发编程中存在的三个概念。原子性问题,可见性问题,有序性问题

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java提供了volatile关键字来保证可见性

有序性:即程序执行的顺序按照代码的先后顺序执行(主要出现问题在多线程)

1.1、volatile

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

通过增加volatile这个关键字,可以禁止程序的优化,从而保证结果的可见性

虚拟机的优化:指令重排序,

  • CPU的高速缓存 :缓存一致性问题

    总线锁(全局锁,降低了cpu的利用率),缓存锁(缓存基于MESI协议)

1、会在修改共享变量的时候增加一个lock指令,将当前处理器缓存行的数据写回到系统内存。

2、这个写会内存的操作会使在其他cpu里缓存了该内存地址的数据无效

操作系统的优化:CPU时间片的调度,CPU调度算法

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2、禁止进行指令重排序。

1.2、synchronized

在并发编程中存在线程安全问题,主要原因有:

1.存在共享数据

2.多线程共同操作共享数据。

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

1.2.1、synchronized的三种应用方式

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁

  • 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁

  • 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

1.2.2、volatile和synchronized区别

1.volatile是变量修饰符,而synchronized则作用于一段代码或方法。volatile的系统可见性是引用系统可见性。
2.volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4.volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

1.3、java实现多线程的三种方式

方法一:继承Thread类

public class MyThread extends Thread{
  @Override
  public void run(){
     System.out.println("创建多线程方法一");
  }
  public static void main(String[] args) {
     MyThread thread=new MyThread();
     thread.start();
     System.out.println("运行结束");
  }

}

方法二: 实现Runnable接口

public class MyThread implements Runnable{
  @Override
  public void run(){
    System.out.println("创建多线程方法二");
  }
  public static void main(String[] args) {
    MyThread thread=new MyThread();
    Thread t=new Thread(thread);
    t.start();
    System.out.println("运行结束");
  }

}

方法三:实现Callable接口

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyThread implements Callable<string>{
public static void main(String[] args) {
  ExecutorService threadPool=Executors.newSingleThreadExecutor();
  //启动多线程
  Future<string> future=threadPool.submit(new MyThread());
  try{
   System.out.println("waiting thread to finish");
   System.out.println(future.get());
  }catch(Exception e){
   e.printStackTrace();
  }
}

  @Override
  public String call() throws Exception {
   return "创建多线程方法三";
  }
}

Callable接口是属于Executor,对比与Runnable接口功能的区别是:
(1).Callable可以在任务结束后提供一个返回值,Runnable没有这个功能
(2).Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常
(3).运行Callable可以拿到一个Future对象,Future独享表示异步计算的结果,它提供了
检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数
的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,
放调用Future的get()方法以获取结果时,当前线程就会阻塞,知道call()方法结束返回结果。

1.4、ThreadLocal

**ThreadLocal解决线程局部变量统一定义问题,**多线程数据不能共享。(InheritableThreadLocal特例除外)不能解决并发问题。解决了:基于类级别的变量定义,每一个线程单独维护自己线程内的变量值(存、取、删的功能)
在这里插入图片描述

1.ThreadLocal类封装了getMap()、set()、get()、remove()4个核心方法。

2.通过getMap()获取每个子线程Thread持有自己的ThreadLocalMap实例, 因此它们是不存在并发竞争的。可以理解为每个线程有自己的变量副本。

3.ThreadLocalMap中Entry[]数组存储数据,初始化长度16,后续每次都是2倍扩容。主线程中定义了几个变量,Entry[]才有几个key。

4.Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象, 防止了内存泄漏。

1.4.1、源码数据
ThreadLocalMap getMap(Thread t) {
  //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
  return t.threadLocals;
}
//set步骤:
//1、根据哈希码和数组长度求元素放置的位置,即数组下标
//2、从第一步得出的下标开始往后遍历,如果key相等,覆盖value,如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据
//3、如果超过阀值,就需要再哈希:清理一遍陈旧数据 >= 3/4阀值,就执行扩容,把table扩容2倍==》注意这里3/4阀值就执行扩容,避免迟滞,把老数据重新哈希散列进新table
public void set(T value) {
  //返回对当前正在执行的线程对象的引用 native方法
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}
// 对应map的set方法
private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1); //根据hash和数组长度获得数组下标的位置
	//从i开始遍历一直遍历到最后一个Entry
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
		//如果key相等那么覆盖value
    if (k == key) {
      e.value = value;
      return;
    }
		//如果key为空直接用新的key,value覆盖,同时清理key = null的旧数据
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  tab[i] = new Entry(key, value);
  int sz = ++size;
  //如果超过阀值那么需要再哈希
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

//get步骤
//1、从当前线程中获取ThreadLocalMap,查询当前ThreadLocal变量实例对应的Entry,如果不为null,获取value,返回
//2、如果map为null,即还没有初始化,走初始化方法
public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);  //从当前线程中获取ThreadLocalMap
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);//查询当前ThreadLocal变量实例对应的Entry
    if (e != null) {//如果不为null,获取value,返回
      T result = (T)e.value;
      return result;
    }
  }
  //如果map为null,即还没有初始化,走初始化方法
  return setInitialValue();
}

private T setInitialValue() {
  T value = initialValue();//该方法默认返回null,用户可自定义
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)//如果map不为null,把初始化value设置进去
    map.set(this, value);
  else//如果map为null,则new一个map,并把初始化value设置进去
    createMap(t, value);
  return value;
}

void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  table = new Entry[INITIAL_CAPACITY];//初始化容量16
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  setThreshold(INITIAL_CAPACITY);//设置阈值
}
//阈值设置为容量的*2/3,即负载因子为2/3,超过就进行再哈希
private void setThreshold(int len) {
  threshold = len * 2 / 3;
}

//看一下Entry的clear方法,Entry ==extends==》 WeakReference<ThreadLocal<?>>==extends==》 Reference<T>,clear方法是抽象类Reference定义的方法
public void remove() {
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    m.remove(this);
}
 private void remove(ThreadLocal<?> key) {
     Entry[] tab = table;
     int len = tab.length;
     int i = key.threadLocalHashCode & (len-1);
     for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         if (e.get() == key) {
             e.clear();//调用Entry的clear方法
             expungeStaleEntry(i);//清除陈旧数据
             return;
         }
     }
 }

再哈希方法

		private void rehash() {
        expungeStaleEntries();// 清理一次陈旧数据

        // 清理完陈旧数据,如果>= 3/4阀值,就执行扩容,避免迟滞
        if (size >= threshold - threshold / 4)
            resize();
    }

    /**
     * 把table扩容2倍,并把老数据重新哈希散列进新table
     */
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;
        // 遍历Entry[]数组
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {// 如果key=null
                    e.value = null; // 把value也置null,有助于GC回收对象
                } else {// 如果key!=null
                    int h = k.threadLocalHashCode & (newLen - 1);// 计算hash值 
                    while (newTab[h] != null)// 如果这个位置已使用
                        h = nextIndex(h, newLen);// 线性往后查询,直到找到一个没有使用的位置,h递增
                    newTab[h] = e;//在第一个空节点上塞入Entry e
                    count++;// 计数++
                }
            }
        }

        setThreshold(newLen);// 设置新的阈值(实际set方法用了2/3的newLen作为阈值)
        size = count;// 设置ThreadLocalMap的元素个数
        table = newTab;// 把新table赋值给ThreadLocalMap的Entry[] table
    }

    /**
     * 删除陈旧的数据
     */
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)//entry不为空且entry的key为null
                expungeStaleEntry(j);//删除指定数组下标的陈旧entry
        }
    }

    //删除陈旧entry的核心方法
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        tab[staleSlot].value = null;//删除value
        tab[staleSlot] = null;//删除entry
        size--;//map的size自减

        // 遍历指定删除节点,所有后续节点
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {//key为null,执行删除操作
                e.value = null;
                tab[i] = null;
                size--;
            } else {//key不为null,重新计算下标
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {//如果不在同一个位置
                    tab[i] = null;//把老位置的entry置null(删除)

                    // 从h开始往后遍历,一直到找到空为止,插入
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

我们知道不管是set、get、remove操作的都是ThreadLocalMap,key=当前线程,value=线程局部变量缓存值。

上图getMap最终调用的Thread的成员变量 ThreadLocal.ThreadLocalMap threadLocals

ThreadLocalMap是ThreadLocal的一个内部类

ThreadLocalMap是一个定制的哈希映射,仅适用于维护线程本地值。ThreadLocalMap类是包私有的,允许在Thread类中声明字段。为了帮助处理非常大且长时间的使用,哈希表entry使用了对键的弱引用。有助于GC回收。

1.4.2、ThreadLocal的内存泄漏

强引用与弱引用

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

GC回收机制-如何找到需要回收的对象

JVM如何找到需要回收的对象,方式有两种:

  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,

  • 可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。

引用计数法,可能会出现A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收。

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

image-20210120133845170

ThreadLocal 内存泄漏的原因

hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

那为什么使用弱引用而不是强引用??

我们看看Key使用的

key 使用强引用

当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

1.4.3、ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

public class ThreadLocalTest2 {

    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}
1.4.4、InheritableThreadLocal类

ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //判断名字的合法性
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //安全校验
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:当前线程组
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //设置为当前线程组
    this.daemon = parent.isDaemon();//守护线程与否(同父线程)
    this.priority = parent.getPriority();//优先级同父线程
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2)如果父线程的inheritableThreadLocal不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}
1.5、ExecutorService
1.5.1、可缓存线程池

Executors.newCacheThreadPool():先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务

1.5.2、可重用固定个数的线程池

Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。

1.5.3、定长线程池

Executors.newScheduledThreadPool(int n) 创建一个定长线程池,支持定时及周期性任务执行

1.5.4、单线程化的线程池

Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

1.5.5、缓冲队列BlockingQueue和自定义线程池ThreadPoolExecutor

缓冲队列BlockingQueue简介:

​ BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。

  1. 常用的几种BlockingQueue:
  • ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
  • LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
  • PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
  • SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。
  1. 自定义线程池(ThreadPoolExecutor和BlockingQueue连用):

    自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池。

常见的构造函数:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ZiDingYiThreadPoolExecutor {

  public static class TempThread implements Runnable {

    @Override
    public void run() {
      // 打印正在执行的缓存线程信息
      System.out.println(Thread.currentThread().getName() + "正在被执行");
      try {
        // sleep一秒保证3个任务在分别在3个线程上执行
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

  }


  public static void main(String[] args) {
    // 创建数组型缓冲等待队列
    BlockingQueue<Runnable> bq = new ArrayBlockingQueue<Runnable>(10);
    // ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
    ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 6, 50, TimeUnit.MILLISECONDS, bq);

    // 创建3个任务
    Runnable t1 = new TempThread();
    Runnable t2 = new TempThread();
    Runnable t3 = new TempThread();
    Runnable t4 = new TempThread();
    Runnable t5 = new TempThread();
    Runnable t6 = new TempThread();

    // 3个任务在分别在3个线程上执行
    tpe.execute(t1);
    tpe.execute(t2);
    tpe.execute(t3);
    tpe.execute(t4);
    tpe.execute(t5);
    tpe.execute(t6);

    // 关闭自定义线程池
    tpe.shutdown();
  }
}
1.6 synchrnoized和reentrantlock的底层实现及重入的底层原理
  • synchronized:

    synchronized用的锁是存在java对象头里的。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步

    反编译解析内容

    Compiled from "LockStudy.java"
    public class com.js.node.LockStudy {
      public com.js.node.LockStudy();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: iconst_2
           1: istore_1
           2: ldc           #2                  // class com/js/node/LockStudy
           4: dup
           5: astore_2
           6: monitorenter
           7: iconst_5
           8: istore_3
           9: aload_2
          10: monitorexit
          11: goto          21
          14: astore        4
          16: aload_2
          17: monitorexit
          18: aload         4
          20: athrow
          21: return
        Exception table:
           from    to  target type
               7    11    14   any
              14    18    14   any
    }
    
    • 1.代码块同步

      使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块开始位置,而monitorexit是插入到方法结束后和异常处。任何对象都有一个monitor与之关联,当它的monitor被某个线程持有后,表示该线程处于锁定状态(在java层面表现为markwork记录当前线程的id)。

      根据虚拟机规范和要求:在执行monitorenter指令时,首先要去尝试获取锁,如果这个锁对象没有被占用 ,或者当前线程已经用了那个锁对象,那就把锁的计数器加1;相应的,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取锁失败了,那当前线程就要阻塞等待,直到锁对象被另一个线程释放。

    • 2.同步方法

      从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),而是被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法。不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

    • 监视器锁monitor

      监视器锁本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都有一个可称为"互斥锁"的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

      互斥锁:用于保护临界区,确保同一时刻只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞直到互斥量被解锁。在完成了对共享资源访问后,要对互斥量进行解锁。

      注意点:

      synchronized同步块对同一条线程来说是可以重入的,不会出现自己把自己锁死的问题。

      同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渡劫-JS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值