synchronized

讲述一下jdk1.6的锁升级过程

无锁->偏向锁->轻量级锁->重量级锁

最开始是个无锁的状态程序进来会先进行判断,当对象第一次被获取的时候,就会设置一个偏向锁,当前获取到锁资源的线程会优先获取到这个线程,如果没有获取到这个锁,就会升级成一个轻量级锁一个cas的锁也就是乐观锁。轻量级锁去获取当前线程的时候如果没有成功的话会进行一个自旋(短暂的等待),自旋到一定程度才会生成一个synchronzied这样重量级的锁

如何保证hashmap中的线程安全(为什么?)

使用ConcurrentHashMap这样线程安全的集合容器,线程安全的还有hashtable,hashtable是jdk1.2中就存在的类型,比较老旧,对增删改查的方法全部变成了同步操作,而ConcurrentHashMap是局部锁定,只对桶枷锁,当前桶被锁定的时候其他桶是不锁定的,同时ConcurrentHashMap是读写分离的,读取不加锁,写的时候加锁,因此性能得到了更好的提升

ConcurrentHashMap与hashmap一样,在jdk1.8之后变成了数组+链表+红黑树,而hashtable只是数组+链表

ConcurrentHashMap上锁的时候使用的还是Synchronized,因为在jdk1.6之中进行了锁升级,所以他的效率和并发度是更高的(相对于lock来说)

synchronized和lock的原理

synchronized是jvm层面的,lock是jdk层面的,synchronized是语义级的支持,是Java中的关键字,而lock是一个类。

synchronized

锁代码块

synchronized原理,每一个对象都会和一个监视器monitor关联,监视器被占用时会被锁住,其他线程无法获取到monitor。当jvm的某个线程去monitorenter时,会去尝试获取当前对象的monitor的所有权,,若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数量置为1。当前线程为monitor的所有者,若线程已经拥有monitor的所有权,当前尝试获取monitor是多有权线程会被阻塞,直到monitor的数量为0,才能重新获取monitor的所有权。

锁方法

在synchronized修饰方法时是添加ACC_Synchonized表示,该标识指明了该方法时一个同步方法,JVM通过ACC_synchronized标志来辨别是否声明为同步方法,从而执行线程的调用

lock

lock其实还是通过cas去修改state的值.lock的基本操作还是通过乐观锁来实现的

没有获取到锁会干以下的事情

  • tryAcquire:会尝试再次通过CAS获取一次锁。
  • addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
  • acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。

Synchronized三种用法以及区别

synchronzied关键字最主要的使用方法如下(可重入)

  • 修饰实例方法,锁的是当前对象,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,锁的是当前class对象,进入同步代码前要先获得当前对象的锁
  • 修饰代码块,锁的是synchronzied括号里配置的对象,指定加锁对象,对给定对象加锁,进入同步代码块钱要获得给定对象的锁

Thread

wait()与sleep()的区别

  • sleep()来自Thread类,和wait()来自Object()类。调用sleep()方法的过程中,线程不会释放对象锁。而调用wait方法线程会释放锁对象
  • sleep()睡眠后不让出资源,wait让其他线程可以占用CPU
  • sleep需要指定一个时间,时间一到自动会唤醒,而wait()需要配合notify()或者notifyAll()使用.

notify和notifyAll区别

等待池:先假设一个线程A调用了莫格对象的wait()方法,线程A就会自动释放该对象的锁后,进入到了该对象的等待池,等待吃中线程不会区竞争该对象的锁

锁池:只有获取了对象的锁,线程才能执行对象synchronized代码,对象的锁每次只有一个线程可以获得,其他的线程只能再锁池中等待

notify()方法随机唤醒线程池中的一个线程,进入锁池;notifyAll()唤醒对象的等待池中的所有线程

start方法与run方法的区别

start与run方法的主要区别在于当程序调用start方法一个新线程将会被创建,并且在run方法中的代码将会在新线程上运行,然而直接调用run方法并不会创建新线程,run方法的代码会在当前线程上运行

thread创建的三种方式

集成Thread类,复写run方法

class Main extends Thread{
    public static int flag;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Main();
        thread.start();
        thread.join();
        System.out.println(flag);
    }

    @Override
    public void run() {
        for (int i=0;i<1000;i++){
            flag++;
        }
    }
}

lambda表达式

class Main {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(11);
        }).start();
        
    }
}

复写runnable()接口,通过构造方法将实现类传入Thread

class Main implements Runnable{
    public static int flag;
    @Override
    public void run() {
        for(int i=0;i<10000;i++){
            flag++;
        }
    }

    public static void main(String[] args) {
        Main mainThread=new Main();
        Thread thread=new Thread(mainThread,"线程1");//第二个参数是名字
        Thread thread2=new Thread(mainThread,"线程2");
        thread.start();
        thread2.start();
        try {
            thread.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(flag);

    }
}

复写callable接口,传入FutureTask的构造方法,再讲FutureTask的类对象传入Thread的构造方法

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Main implements Callable<String> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String>task=new FutureTask<>(new Main());
        new Thread(task).start();
        System.out.println(task.get());
    }

    @Override
    public String call() throws Exception {
        System.out.println("线程执行");
        return "线程执行完毕";
    }
}

讲述一下ThreadLocal和ThreadLocal如何解决内存泄漏问题

threadlocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量都是相互独立的。通过get和set方法就可以得到当前线程的对应值,做个不恰当的比喻,threadlocal相当于维护了一个map,key就是线程,value就是需要存储的对象。

ThreadLocal的实现原理是每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。 如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收,这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value。

解决方法是每次使用完threadlocal都调用它的remove()方法清除数据或者按照jdk建议将变量定义成private static,这样就一直存在对thread的强引用,也就能保证任何时候能通过Threadlocal的弱引用访问到entry的value值

img

线程池的运行的流程,参数和策略

参数有,核心线程数大小,最大线程数,任务队列长度,拒绝策略,空闲线程存活时长.

当任务大于核心线程数的时候,就开始吧任务往存储任务的队列里加,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当前线程数达到了最大,就开始执行拒绝策略,比如记录日志或者是直接丢弃.

volatile

CAS的aba问题如何解决

cas的实现过程是先取出内存中某时刻的数据,在下一时刻比较并交换,那么在这个时间会导致数据的变化,此时就会导致出现ABA问题

比如一个线程one从内存位置中取出A,这时候另一个线程也从内存中取出A,并且执行了一些操作变成B,然后又将数据变成了A,这时候进行CAS操作发现内存中任然是A,然后one操作成功

尽管one的CAS操作成功,但这不代表这个过程中没有问题

可以通过额外加一个时间戳字段每次更新的时候就更新这个时间戳,或者添加一个版本号字段,每次更新的时候都把版本号自增1,每次要对数据进行改动的时候可以多校验一个版本号或者时间戳.

谈一下AQS,为什么AQS的底层为什么是CAS+volatile

AQS是抽象的队列式同步器.是除了java自带synchronized关键字之外的锁机制,在java.util.concurrent.locks包下.java的其他并发工具都是依赖于AQS的,其中维护了一个用volatile修饰的state属性来表示资源的状态,子类需要定义和维护这个状态来控制如何获取锁和释放锁,它底层是CAS(比较并交换)也就是一个乐观锁,使用volatile能保证该属性的可见性,以及保证了代码的有序性.线程通过CAS去改变状态,成功则获取所成功1,失败则进入等待队列,等待被唤醒

volatile如何保证可见性和有序性

导致可见性问题的发生原因是java内存模型中分为工作内存和主内存,vlotile中存在读屏障和写屏障,写屏障能保证在改屏障之前的,对共享变量的改动都同步到主存中,读屏障保证该屏障之后,对于共享变量的读取加载的是最新的数据.同时写屏障能保证之前的代码不会指令重排序到后面,读屏障能保证之后的代码不会指令重排序到到前面

DCL问题为什么要加volitale

dcl就是所谓的doble-checked loncking双检查的锁模式

在多线程的环境下,双检查锁的模式其实是有问题的,因为有变量没有被synchronized完全保护到,而synchronized只有完全保护一个变量才能解决原子性可见性有序性的问题.在多线程环境下new一个对象底层并不是一个原子性操作,而是以下四步

  • 在堆创建一个对象,将对象引用入栈
  • 复制一份对象的引用
  • 利用对象的引用调用构造方法
  • 将对象的引用赋值给变量

也许jvm会优化为先将对象的引用赋值,再调用构造方法,使用volatile读取操作之前会加上读屏障,在volatile写操作之后会加上写屏障,保证了代码的有序性和可见性

对as-if-serial和happens-before的理解

as-if-serial

as-if-serial语义的意思指:不管如何进行重排序,程序的执行结果不能改变1,为了遵循as-if-serial语义,编译器和处理器不会对存在数据依赖关系做重排序,因为这种重排序会改变执行结果.但操作之间不存在数据依赖关系的操作会被做重排序

happens-before

因为jvm会对代码进行优化,指令会有重排序的情况,为了避免编译优化对并发变编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,并保证并发编程的正确性

它有如下几条规则:

程序次序规则:在一线程内,一段代码的执行结果是有序的.它的指令会进行重排序,但按照指令生成的结果是不会变的

管程锁定规则就是无论在单线程还是多线程环境,对同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这所都能看到前一个线程的操作结果(synchronized就是管程的实现)

volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么写操作一定对读这个线程可见

线程启动规则:在主线程A执行过程中,启动子线程B,那么A在启动子线程B之前对共享变量的修改结果对线程b可见

线程终止规则:在主线程A执行过程中,子线程b终止,那么b在终止之前对于共享变量的修改结果在A中可见

线程中断原则:线程1打断线程2前对变量的写,对于其他得知线程2被打断后,对变量的读是可见的(interrupt()打断和isInterrupted()来判断是否被打断)

传递规则:happens-before原则具有传递性,比如对x声明了volatile,之后在x定义之前定义了变量y的值,之后定义了x的值,那么x与y的值对于其他线程都是可见的

默认值:对变量默认值0,false,null的写,对于其他线程是可见的

死锁的四个必要条件

死锁是指多个进程因资源因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都无法向前推进

产生原因有俩点1系统的资源竞争导致系统资源不足,以及资源分配不当,进程运行推进顺序不适合,请求和释放的资源顺序不当会造成死锁

死锁的四个必须条件

  • 互斥条件:一个资源只能被一个进程使用,此时若其他进程请求该资源,则请求进程只能等待
  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求而该资源已被其他进程占有,此时请求进程被阻塞,但对自己获得的资源保持不放
  • 不可剥夺条件:进程获的的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的进程来自己释放
  • 循环等待条件:若干进程间形成收尾相接循环等待资源的关系

这四个是死锁发生的必要条件,只要系统发生死锁,这些条件必然成立,只要上述条件之一不满足,就不会发生死锁

如何避免死锁

我们可以通过破坏锁产生的四个必要条件来预防死锁,由于资源互斥是资源使用的固有特性是无法被改变的

1:破坏不可剥夺条件,一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统资源的列表中,可以被其他进程使用,而等待的进程只有重新获得自己原有的资源以及申请的资源才可以重新启动

2.破坏请求保持"条件:第一种方法静态分配即每个进程在开始执行时就申请他锁需要的全部资源.第二种是动态分配即每个进程在申请所需要的资源时,它本身不占用系统资源

3破坏"循环条件":采用有序分配将系统中所有的资源按照紧缺到稀少进行编号,将紧缺的使用较小编号,稀少采用较大编号,在申请资源时按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

Last modification:July 29, 2022
如果觉得我的文章对你有用,请随意赞赏