JAVA面试3-多线程并发和锁

137 阅读5分钟

1、线程创建的三种方式

线程创建出来是为了完成任务,任务是通过run方法来完成的,而run方法来自于Runnable接口

1.1 线程与任务合并(继承Thread类)

       public class Student extends Thread{
          public void run(){
             //耗时的操作
             for(int x = 0 ; x < 200 ; x ++){
             //这个类继承Thread类,获取线程名称 
  	     System.out.println(this.getName()+":"+"hello"+x);
                }
            }
            
            或者:
            new Thread(
                new Runnable(){
                    public void run(){
                        sout("hello world");
                    }
                }
            ).start();

1.2 线程与任务进行分离(thread类+实现runnable接口)

假若有多个线程,且实现的任务是一样的,则对应的每个任务都有好多的代码

    //任务类
    class MyRunnable implements Runnable{
        public void run() { for(int x = 0 ; x < 100 ; x ++){
        System.out.println(Thread.currentThread().getName()+":"+x);
    }
    
    main:
    //创建任务类的对象 
    MyRunnable m = new MyRunnable();
    //将任务添加到线程内,线程是为了完成任务
    Thread t1 = new Thread(m);
    Thread t2 = new Thread(m);

runnable接口的实现有一个缺陷,无法返回任务的结果

1.3 实现Callable接口

但是Thread不接受实现Callable对应实现类的对象,需要将对应的实现类封装成Runnable类型,传给Thread的构造函数

calss MyCallable implements Callable<Integer>{
    //callable对应的是call方法,进行重写
}

MyCallable m1 = new MyCallable();
Thread t1 = new Thread(m1); //加不进去

`FutureTask本质是Runnable接口和Future接口的实现类`
FutureTask<Object> ft1 = new FutureTask<>(m1); 
//FutureTask将对象包装为Runnable的对象,才能将任务加入到线程中

Thread t1 = new thread(ft1);

2、线程的基础

进程是资源分配的单位,但进程的调度的好费时间,后来引出线程,取代了进程调度的功能

2.1 进程之间的通信方式

2.1.1 java线程

2.1.2 os的线程

操作系统的四个特征:并发、共享、虚拟和异步 并发:宏观上是同时发生的,微观是交替发生的 并行:同一时刻发生的
共享:对临界资源的访问

2.2 死锁

两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。

多线程产生死锁的四个互斥条件

  • 互斥条件: 一个资源每次只能被一个进程使用。
  • 请求并保持: 一个进程因请求资源而阻塞时,对已获得资源保持不放。
  • 不可剥夺性: 进程已获得资源,在未使用完成前,不能被剥夺。
  • 循环等待条件(闭环): 若干进程之间形成一种头尾相接的循环等待资源关系。 破坏四个互斥条件中的其中一个,将会避免死锁

2.3 线程的状态

  • new:新建,三种方式,继承thread(线程和任务在一起) 实现Runnable接口(线程和任务进行分离) 实现callable接口。解决Runnable接口不能返回空值,需要重写call方法
  • runnable(可运行),启动start方法,线程不是马上运行,而是等待线程的调度
  • running:获得了CPU的时间片。执行程序代码
  • 阻塞 等待阻塞,运行的线程执行o.wait()方法,JVM会把该线程放入等待的队列中
    同步阻塞,运行的线程在获取对象同步锁的时候,同步锁被别的线程放入锁池中
    线程执行sleep方法,JVM会线程设置为阻塞状态。但不会释放锁。当sleep状态超时、join等待线程终止或超时,线程重新进入runnable状态

wait是object的方法sleep是Thread的静态方法。wait会释放锁,而sleep不会释放锁

2.4 线程同步(多线程)

2.4.1 多线程可能会导致的问题:原子性、有序性、可见性

原子性:automic包下的类
可见性:volatile关键字
有序性:JMM的happens-before as-if-serial
集合:JUC
加锁:在监视器的内部。监视器和锁在java虚拟机中是一块使用的。监视器监视一块同步代码
块,确保一次只有一个线程执行。每一个监视器和一个对象的引用相关联。提供了显示监视器
(lock)IDK和隐式监视器( synchronized )JVM两种锁方案。

2.4.2 java线程之间的通信,即线程之间的交互

线程通信涉及到的方法 线程通信涉及到的三个方法:
(1)wait():一旦执行此方法当前线程进入阻塞状态,并释放同步监视器 (锁)
(2)notify():一旦执行此方法就会唤醒被wait的另一个线程。如果存在多个被wait的线程,唤醒优先级高的线程。 (3)notifyAll():一旦执行此方法就会唤醒所有被wait的线程
以上的方法都是Object中的方法,因为synchronized加锁是加载对象上面的,每一个对象都具有这样的方法
说明:
(1)wait()、notify()、notifyAll()三个方法必须在同步代码块或同步方法里面使用
(2)wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法里面的同步监视器,obj对象作为同步监视器的话其他地方调用这个三个方法也需要使用obj对象
(3)因为任何一个对于多线程来说是唯一的对象都可以充当同步监视器,所以上面三个方法是定义在Object对象里面的

三、多线程-》锁机制

3.1 CAS机制

CAS是一种乐观锁的实现机制,只在提交数据更新的时候进行判断,是否和内存中的值相等,如果相等则比较并交换,如果不等,则不会操作。相等于无锁

同样,如果出现ABA问题,使用了一个JAVA的类 AtomicStampedReference(原子标记参考),不在只比较值,还比较对象的版本号

3.2 锁(JVM层面的隐士加锁,相当于一个监视器)

是java中的一个关键字,可以加载实例方法,静态方法及代码块,对应的都是锁住的对象

因此,对象的进行分析,对象包括对象头,对象实例数据和对象填充,其中对象头的mark word重点关注

反编译来看,主要依赖mointerenter和monitorexit指令,每一个加synchronized对象都有一个与之对应的mointor对象,由JVM进行创建,monitor锁对象包含标有锁的进程以及等待锁的线程队列

1.png

对象主要包含三个部分,对象头(Mark word 和 类对象地址的指针)

2.png 其中的mark word是一个32为的数据结构,存着0和1

  • 无锁的状态:当我的偏向锁标志是0锁标志位是01,也就是最后3位是001的时候,我表示无锁模式。作为Mark Word的我就是记录的数据就是对象的hashcode 和 GC的年龄 3.png
  • 当我的偏向锁标志是1锁标志是01,也就是最后三位是101的时候,处于偏向锁模式,我作为Mark Word这个时候记录的数据就是是获取偏向锁的线程IDEpoch对象GC年龄

4.png

  • 当我的锁标志位是00的时候,表示处于轻量级锁模式。我会把锁记录放在加锁的线程的虚拟机栈空间中,所以这种情况下锁记录在哪个线程虚拟机栈中,就表示所在线程就获取到了锁

5.png

  • 当我锁标志位是10的时候,表示处于重量级锁模式,这个时候就说明竞争激烈了,处于重量级锁模式了,由于使用重量级加锁不是我的职责范围是我的哥们monitor的职责,我这里有它的地址,你们去那里找他吧。
    这个是我作为Mark Word 记录的数据就是我哥们monitor的地址,你们有加锁的需求直接根据我提供的这个地址找到monitor,找它加锁就好了。

6.png

后三位:偏向锁标志为1位 锁标志为2位 0 01无锁状态 1 01偏向锁 00轻量级锁 10重量级锁

总结

  • 当是0 01无锁状态的时候,markword中前29为分别是hachcode和GC的年龄
  • 当为1 01的时候,为偏向锁,开始改变mark word中的值,变为指定线程的ID
  • 当为 00的时候,为轻量级锁,将锁记录放在线程私有的虚拟机栈中,谁能与之对应的mark word的锁记录,则谁获得轻量级的锁
  • 当为10的时候,前面的mark word无能为力,只能寻找monitor对象,对应前面的地址指向monitor对象的地址

3.2.1 重量级锁的执行过程:

nomitor对象,当mark word的后两位是10的时候,指向monitor对象来实现重量级锁。其实其实monitor在底层也是某个类的对象,那个类就是ObjectMonitor

主要有以下几个属性字段:

  • _count ; // 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示加锁的次数
  • _owner; // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁
  • _waitset; // wait线程的集合,在synchorized代码块中调用wait()方法的线程会被加入到此集合中沉睡,等待别人叫醒它
  • _entrylist; // 非常重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次争抢锁
  • _spinFreq; // 获取锁之前的自旋的次数
  • _spinclock; // 获取之前每次锁自旋的时间

加锁的过程:首先没有对monitor进行加锁的时候。_count=0表示加锁次数是0,也就是没有线程进行加锁;_owner指向null,没有线程加锁。然后此时有A、B,同时请求将_count修改为1,这个操作具有原子性。同一时刻只有一个线程修改成功;谁能将_count修改为1,谁就能获取到锁,同时将_owner指向自己,表示时哪个线程加了锁。释放锁的时候直接将count为0就可以了。 此时线程B来了,想要获取到对象,发现此时已经被A线程获取,monitor里的字段_spinclock,_spinFreq分别表示请求失败旋的时间和次数,当超过指定次数的时候,则会加入到等待队列中进行休眠

为啥使用自旋?
挂起之后唤醒的代价很大,底层涉及到上下问的切换,防止短时间内释放资源导致时间和资源的浪费

wait(),notify(),notifyAll()都是object中的方法,分析一下? wait()方法是,线程A首先获得锁,发现自己某些条件不满足,此时执行wait()方法,进行释放锁,加入到waitset集合中,释放锁,等待别人进行唤醒 线程B获取到锁,执行对应的方法,加锁,然性执行notify唤醒waitset中一个,或者执行notifyAll()唤醒所有线程

3.3 锁重入、锁消除、锁升级 无锁、偏向锁、轻量级锁、自旋、重量级锁

3.3.1 锁重入

加锁就是将monitor类的对应的父类ObjectMonitor中的字段_count由0设置为1,将owner指向加锁的线程。当再重入锁的时候,检查owner是否给自己加锁的,给自己加锁只需要将_count次数加1即可。释放锁的时候,执行monitorexit指令,首先将count进行减1,当count减少到0的时候,表示自己释放了锁

3.3.2 锁消除

锁消除就是在不存在锁竞争的地方使用了Synchronized,jvm会自动进行优化

3.3.3 锁升级

目的:花费最小的代价能到达目的

  • 当线程第一次进入synchronized的同步代码块之内,发现标志为001,于是自己加了偏向锁,这样的好处是第一次加锁的时候执行CAS操作将偏向锁的标志位改为1,并把自己的线程ID到mark word中之后线程A每次获取锁的时候都不需要加锁,直接执行
  • 问题:加锁的时候线程的ID已经加到mark word中,不会在释放锁,修改锁的mark word信息不会在修改过来,不会主动的释放锁,但是别的线程想要使用synchronized代码块怎么办?
  • 当发现线程A不存活了,则直接进行重偏向,将markword的线程ID改为线程B。但是如果A没结束,但是代码块执行完了,也可以进行重偏向。若都没有结束,则需要进行锁升级
  • 加锁之前创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。
  • 这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁
  • 线程A持有偏向锁,这是B来了,不能老让A一直执行,将线程A进行暂停,为线程A创建一个Lock record放在虚拟机栈中,将mark word中的值复制到lock record中,然后将前三十位指向线程A的所记录地址,将线程A唤醒则线程A知道自己持有了轻量级锁,标志位变为00
  • 线程A和B同时竞争锁,在轻量级的锁的模式下,都会将Lock Record锁记录放入自己的栈针中,同时执行CAS操作,谁设置成功,谁获取到锁。加锁的过程,轻量级锁的释放很简单,就将自己的Lock Record中的Mark Word备份的数据恢复回线程的虚拟机栈中即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。

参考:blog.csdn.net/chenzengnia…