常见面试题(三)

分类专栏:
面试题分享

文章标签:
Java自学
常见面试题
原创

一.synchronize关键字

使用方式:

1)修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

举例单例对象双重校验锁
public class Singleton{

    private volatile static Singleton singleton;

    private Singleton(){}

    public static Singleton getSingleton(){
		//先判断对象是否已经实例过,没有实例化过才能进入加锁代码
        if (singleton==null){
            //类对象加锁
            synchronized (Singleton.class){
                if (singleton==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

注意singleton采用volatile关键字 singleton = new singleton() 这段代码分三步执行

为singleton分配内存空间

初始化singleton

将singleton指向分配的内存地址

但是由于JVM具有指令重排的特性执行顺序可能会发生改变,使用volatile可以禁止JVM的指令重排,保证在多线程的环境下也能正常运行

synchronize关键字底层原理

1)同步语句块情况

public class synchronizeDemo {
    
    public void method() {
        
        synchronize(this){
            System.out.println("synchronize 代码块");
        }
    } 
}

通过JDK自带的命令查看类相关字节码信息

synchronize同步语句块实现使用的是monitorentermonitorexit指令

monitorenter指令指向同步代码块的开始位置,monitorexit指令指明同步代码块的结束位置

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权

当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1

相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放

如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

2)synchronize修饰方法的情况

public class synchronizeDemo2 {
    
    public synchronize void method() {
        System.out.println("synchronize 方法");
    }
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是

ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用

synchronize和volatile

volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronize关键字要好

volatile关键字只能用于变量而synchronize关键字可以修饰方法以及代码块

多线程访问volatile关键字不会发生阻塞,而synchronize关键字会发生阻塞

volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronize皆可保证

volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronize关键字解决的是多个线程之间访问资源的同步性

二.线程池

线程池提供了一种限制和管理资源。每个线程池还维护一些基本统计信息,例如已完成任务的数量

使用线程池好处:

1)降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗

2)提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行

3)提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配,调优与监控

实现Runnable接口和Callable接口的区别

实现的runnable接口和callable接口实现类都可以被ThreadPoolExecutor或ScheduleadThreadPoolExecutor执行

两者的区别在于Runnable接口不会返回结果但是callable接口可以返回结果

工具类Executors可以实现Runnable对象和Callable对象之间的相互转换

(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule) )

execute()方法与submit()方法

1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否

2)submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout ,Timeout unit)方法则会阻塞当前线程一段时间后立即返回,这时候任务有可能没有执行完

如何创建线程池

阿里巴巴Java开发手册中强制建议线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式

Executor返回线程池对象的弊端:

FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(全称“Out Of Memory”,翻译成中文就是“内存用完了”)

CachedThreadPool和ScheduleadThreadPool:允许创建的线程数据为Integer.MAX_VALUE,可能创建大量线程,从而

导致OOM(全称“Out Of Memory”,翻译成中文就是“内存用完了”)

建议创建方式:通过构造方法实现

三.Atomic原子类

Atomic 翻译成中文是原子的意思,这里 Atomic 是指一个操作是不可中断的

即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

所以,所谓原子类说简单点就是具有原子/原子操作特征的类

并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下

JUC包中的原子类四种类型

基本类型

使用原子的方式更新基本类型

AtomicInteger 整形原子类
AtomicLong 长整型原子类
AtomicBoolean 布尔型原子类
数组类型

使用原子的方式更新数组里的某个元素

AtomicIntegerArray 整形数组原子类
AtomicLongArray 长整形数组原子类
AtomicReferenceArray 引用类型数组原子类
引用类型
AtomicReference: 引用类型原子类
AtomicStampedRerence 原子更新引用类型里的字段原子类
AtomicMarkableReference 原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater 原子更新整形字段的更新器
AtomicLongFieldUpdater 原子更新长整形字段的更新器
AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新可能出现的ABA问题

AtomicInteger使用

AtomicInteger类常用方法
public final int get() //获取当前的值 
public final int getAndSet(int newValue)//获取当前的值,并设置新的值 
public final int getAndIncrement()//获取当前的值,并自增 
public final int getAndDecrement() //获取当前的值,并自减 
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) 
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程 在之后的一小段时间内还是可以读到旧的值
使用AtomicInteger之后,不用对increment()方法加锁也可保证线程安全
class AtomicIntegerTest { 
    private AtomicInteger count = new AtomicInteger(); 
    //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全
    public void increment() { 
        count.incrementAndGet(); 
    }
    public int getCount() { 
        return count.get(); 
    } 
}
简单描述下AtomicInteger类的原理
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) 
private static final Unsafe unsafe = Unsafe.getUnsafe(); 
private static final long valueOffset; 

static { 
    try {
        valueOffset = unsafe.objectFieldOffset 
            (AtomicInteger.class.getDeclaredField("value")); 
    } catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;

AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronize的高开销,执行效率大为提升

CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值

UnSafe 类的 objectFieldOffffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffffset

另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值

四.AQS

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的

原理:

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资设

置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结

点之间的关联关系

AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配

在这里插入图片描述

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作

AQS使用CAS对该同步状态进行原子操作实现对其值的修改

//共享变量,使用volatile修饰保证线程可见性
private volatile int state; 

状态信息通过protected类型的getState,setState,compareAndSetState进行操作

//返回同步状态的当前值 
protected final int getState() { 
    return state; 
}
// 设置同步状态的值 
protected final void setState(int newState) { 
    state = newState; 
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) 
protected final boolean compareAndSetState(int expect, int update) { 
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update); 
}

AQS定义两种资源共享方式

1)Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁与非公平锁

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

非公平锁:当线程要获取锁时,无视队列顺序直接抢锁,谁抢到谁拿到锁

2)Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch,Semaphore,

CountDownLatCh,CyclicBarrier,ReadWriteLock

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读

不同的自定义同步器争用共享资源的方式也不同

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了

AQS底层使用模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应

用):

1)使用者继承AbstractQueuedSynchronizer并重写指定的方法(这些重写方法很简单,无非是对于共享资源state的获取和释放)

2)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法,这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用

自定义同步器需重写下面几个AQS提供的模板方法
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false

默认情况下,每个方法都抛出 UnsupportedOperationException

这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞

AQS类中的其他方法都是fifinal ,所以无法被其他类使用,只有这几个方法可以被其他类使用

AQS组件总结:

Semaphore(信号量)-允许多个线程同时访问:synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源

CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行

CyclicBarrier(循环栅栏):CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活

CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞

此处整理来自JavaGuide

图片来自本人CSDN

  • 作者:潘震
  • 评论

    留言