java面试题-并发关键字(Synchronized,volatile,final)

Synchronized

1.Synchronized可以作用在哪里?

Synchronized可以作用在方法、代码块、静态方法和类上。

方法

public synchronized void method(){
    //同步代码块
}

代码块

Object lock = new Object();
synchronized(lock){
    //同步代码块
}

静态方法

public static synchronized void staticMethod(){
    //同步代码块
}

Class clazz = MyClass.class;
synchronized(clazz){
    //同步代码块
}

2.Synchronized本质上是通过什么保证线程安全的?

synchronized关键字的底层实现是基于Java对象头(Object Header)和monitor实现的。

每个Java对象在内存中都有一个对象头,用于存储对象的元数据信息,包括对象的哈希码、锁状态标识、GC标记等。当一个线程访问被synchronized修饰的方法或代码块时,它会先尝试获取该对象的monitor(监视器)。

当一个线程成功获取对象的monitor时,该线程就可以进入临界区(Critical Section)执行同步代码块。在进入临界区之前,线程会将对象的锁状态标识设置为“locked”,表示该对象被当前线程占用,其他线程需要等待。当一个线程执行完同步代码块后,它会释放对象的monitor,同时将对象的锁状态标识设置为“unlocked”,这样其他线程就可以继续竞争该对象的锁,进入临界区执行同步代码块。

在Java 5之后,Java引入了基于锁升级的机制,即在Java对象头中添加了一个字段叫做“Mark Word”,用于记录对象的锁状态和其他信息。当一个线程在获取对象锁时,如果发现对象的锁状态是“unlocked”,则它会尝试使用CAS(Compare-And-Swap)指令将对象的锁状态改为“locked”,从而避免使用操作系统级别的互斥量(Mutex)造成的性能开销。如果CAS指令失败,那么就会使用操作系统级别的互斥量来实现锁,这时会将线程阻塞并放入对象的等待队列中。在Java 6之后,JVM还引入了偏向锁和轻量级锁机制,进一步提高了锁的性能。

总之,synchronized关键字的底层实现是基于Java对象头和monitor实现的,通过获取对象的锁来保证线程安全。随着Java虚拟机技术的不断发展,锁机制也在不断优化和升级,以提高并发性能和降低锁的开销。

3.Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?

我们可以考虑以下方法:

  1. 减小同步块的范围。尽量减小同步块的范围,只保护必要的共享资源,这样可以缩小锁的粒度,减少线程等待和上下文切换的开销。

  1. 使用局部变量替换共享变量。在多线程环境下,共享变量的访问比局部变量的访问开销更大。如果某个变量只在某个方法或代码块内部使用,那么就可以使用局部变量来代替共享变量,从而减少同步块的范围。

  1. 使用volatile关键字。volatile关键字可以保证变量的可见性和有序性,同时避免了锁的开销,因此可以在一些轻量级的同步场景中使用。

  1. 使用Lock接口。Java提供了Lock接口和其实现类ReentrantLock,可以替代synchronized关键字实现同步,Lock接口提供了更丰富的同步机制,比如支持超时和可中断等操作,同时也允许多个线程同时访问共享资源,从而提高了并发性能。

  1. 使用原子变量。Java提供了一些原子变量类,比如AtomicInteger、AtomicLong、AtomicReference等,这些变量可以实现一些基本的原子操作,避免了锁的开销,从而提高了并发性能。

4.Synchronize有什么样的缺陷? Java Lock是怎么弥补这些缺陷的?

Synchronized作为Java中最基本的同步机制,虽然使用简单,但也存在一些缺陷,主要包括以下几个方面:

  1. 性能问题:Synchronized的性能相对较低,因为它会导致线程之间频繁地竞争锁资源,从而导致上下文切换和线程阻塞等开销,影响程序的并发性能。

  1. 灵活性问题:Synchronized只支持一种锁机制,且不支持可重入、可中断、超时等操作,无法满足一些复杂的同步需求。

  1. 可见性问题:Synchronized只能保证共享变量在锁释放时的可见性,而不能保证变量的实时可见性,因此需要配合volatile关键字等其他机制来使用。

为了弥补Synchronized的这些缺陷,Java提供了Lock接口和其实现类,主要包括ReentrantLock、ReentrantReadWriteLock等。这些类通过以下几个方面来提高并发性能:

  1. 性能优化:Lock实现类可以实现更细粒度的控制,支持更灵活的加锁和解锁操作,可以避免Synchronized的性能问题。

  1. 灵活性提升:Lock接口和实现类提供了更多的同步机制,如可重入锁、读写锁等,同时也支持可中断、超时等操作,能够更好地满足各种复杂的同步需求。

  1. 可见性保障:Lock接口和实现类支持更细粒度的锁定和解锁操作,可以保证共享变量的实时可见性,避免了Synchronized的可见性问题。

  1. 公平性控制:Lock实现类可以通过构造函数参数来控制锁的公平性,从而避免线程饥饿等问题。

需要注意的是,Lock接口和实现类的使用需要手动管理锁的获取和释放,而且需要使用try...finally语句块确保锁的释放,否则可能会导致死锁等问题。此外,Lock的使用也需要注意一些细节问题,如使用tryLock()方法尽量避免死锁、使用ReentrantReadWriteLock来提高读操作的并发性能等。

5.Synchronized和Lock的对比,和选择?

  • Synchronized

使用简单,不需要手动加锁

支持可重入性和线程的可见性锁的释放由JVM自动管理,不容易出现死锁

  • Lock

支持公平锁和非公平锁,更加灵活

支持可中断性,可以避免死锁性能较好,

性能较好,在高并发场景下比Synchronized更快

在选择Synchronized和Lock时,需要根据具体的应用场景和需求进行选择。如果不需要过多的控制锁的粒度,只需要简单的锁定某个对象,那么使用Synchronized是一个不错的选择。如果需要更加灵活的锁控制、更好的性能、更好的可中断性等特性,那么可以选择Lock。同时,在使用Lock时,需要注意避免出现死锁等问题。

6.Synchronized在使用时有何注意事项?

  1. 尽量缩小锁的作用范围;

  1. 避免死锁;

  1. 不要将Synchronized作用于静态变量;

  1. 对锁的释放要及时;

  1. 避免使用String类型作为锁对象;

  1. 尽量避免在同步代码块中执行耗时的操作。

7.Synchronized修饰的方法在抛出异常时,会释放锁吗?

如果在执行Synchronized修饰的方法或代码块时抛出异常,JVM会自动将锁释放掉,以避免死锁的情况。因此,在使用Synchronized时,需要及时释放锁,以避免死锁等问题。

8.synchronized是公平锁吗?

在Java中,Synchronized锁默认情况下是非公平的锁。

9.多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?

非公平锁

当有多个线程等待同一个Synchronized锁时,JVM并不会按照线程等待的先后顺序来选择下一个获取锁的线程。相反,它会通过一定的调度算法来选择下一个获取锁的线程,可能会导致某些线程一直无法获取锁,产生饥饿问题。


volatile

1.volatile关键字的作用是什么?

volatile关键字的作用是保证可见性和有序性,即保证所有线程都能看到它的最新值,禁止指令重排序优化,但不能保证原子性。

也就是说,多个线程同时修改同一个volatile变量时,可能会出现并发问题,因此,在需要保证原子性的场合,仍需要使用Synchronized或者Lock等其他机制来进行控制。

2.32位机器上共享的long和double变量的为什么要用volatile?

因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。

使用volatile关键字可以解决这个问题,因为volatile关键字可以保证变量的可见性和禁止指令重排,即保证一个线程修改了这个变量的值,其他线程可以立即看到这个变量的修改值,而且JVM不会对volatile变量的读写操作进行指令重排序优化,保证操作的有序性。

因此,当多个线程并发读写long和double类型的变量时,可以将其声明为volatile变量,从而保证线程安全。

3.volatile是如何实现可见性的?

volatile关键字可以保证变量的可见性,即一个线程修改了volatile变量的值,其他线程可以立即看到最新的值。实现原理是在写入volatile变量时,使用内存屏障指令将修改后的值刷新回主内存;在读取volatile变量时,使用内存屏障指令从主内存中读取最新的值。

4.volatile是如何实现有序性的?

volatile关键字可以保证变量的有序性,即对一个volatile变量的写操作和读操作都不能重排序。实现原理是在写入volatile变量时,在写操作之前插入内存屏障指令,保证在写操作完成之前,不会重排序到写操作之后的代码;在读取volatile变量时,在读操作之后插入内存屏障指令,保证在读操作完成之后,不会重排序到读操作之前的代码。这样可以保证volatile变量的读写操作按照代码的顺序执行。

5.说下volatile的应用场景?

  1. 状态标记:当一个线程修改了一个共享状态标记时,使用volatile关键字可以确保其他线程能够立即看到最新的状态,从而避免了死锁和饥饿等问题。

public class MyRunnable implements Runnable {
    private volatile boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            // do something
        }
    }

    public void stop() {
        flag = false;
    }
}
  1. 双重检查锁定(Double Checked Locking):当一个线程要获取一个单例对象时,使用双重检查锁定可以避免多个线程同时创建对象的问题。需要注意的是,双重检查锁定在Java 5之前是不安全的,因为对volatile关键字的实现不够完善。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 线程通信:当多个线程之间需要进行通信时,可以使用volatile关键字保证通信的可见性和有序性,从而避免了死锁和饥饿等问题。

6.使用 volatile 必须具备的条件

  1. 变量状态不依赖于先前的状态:如果变量的值取决于先前的状态,那么使用 volatile 不能保证线程安全,需要使用其他的同步手段,如 synchronizedLock

  1. 变量不需要与其他状态变量共同参与不变约束:如果需要多个变量的状态一起满足某个不变条件,那么使用 volatile 也不能保证线程安全,需要使用其他的同步手段。

  1. 对变量的写操作不依赖于当前值:如果对变量的写操作依赖于当前值,那么使用 volatile 不能保证线程安全,需要使用其他的同步手段。

  1. 对变量的访问不需要加锁:如果需要对变量进行复合操作,例如“先检查再更新”操作,那么使用 volatile 不能保证线程安全,需要使用其他的同步手段。


final

1.所有的final修饰的字段都是编译期常量吗?

不是所有使用 final 修饰的字段都是编译期常量。在Java中,final 修饰的字段有两种类型:

  1. 编译期常量:使用 final static 修饰的字段被视为编译期常量,也就是类加载时就被初始化,并且在编译时就能确定其值。这种类型的字段可以直接使用类名进行访问,且访问速度很快,因为它们已经在编译期间被优化了。

  1. 运行期常量:使用 final 修饰的非静态字段被视为运行期常量,这意味着它们在运行时才被初始化,并且它们的值只能被赋值一次。这种类型的字段通常用于不变对象或常量池中的值。

因此,只有使用 final static 修饰的字段才是编译期常量,而不是所有使用 final 修饰的字段。

2.如何理解private所修饰的方法是隐式的final?

在Java中,使用 final 关键字修饰的方法不能被子类重写。如果在父类中使用 private 关键字修饰方法,则子类无法继承该方法,更不可能重写它。因此,对于使用 private 修饰的方法,它们默认就是隐式的 final 方法。

final 关键字的作用是让方法或变量的值在定义后不能被修改。对于私有方法而言,它们只能在本类中被调用,其他类无法调用它们,更不可能修改它们。因此,在这种情况下,使用 final 关键字并没有任何实际作用。

举个例子,假设有以下父类:

public class Parent {
    private void doSomething() {
        // ...
    }
}

如果在子类中定义了一个同名的私有方法:

public class Child extends Parent {
    private void doSomething() {
        // ...
    }
}

则这个子类方法并不是对父类方法的重写,而是一个新定义的私有方法。这意味着,无论父类中的方法是否使用 final 关键字,子类都无法对其进行重写。

3.说说final类型的类如何拓展?

使用 final 修饰的类是不能被继承的,所以不能直接拓展。但是可以使用以下方式来实现拓展:

以下是使用包装器类的示例代码,假设有一个 finalMyFinalClass

public final class MyFinalClass {
    private final int value;

    public MyFinalClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

然后可以创建一个包装器类 MyWrapperClass,并在其中添加一些额外的方法或属性:

public class MyWrapperClass {
    private final MyFinalClass myFinalClass;

    public MyWrapperClass(MyFinalClass myFinalClass) {
        this.myFinalClass = myFinalClass;
    }

    public int getValue() {
        return myFinalClass.getValue();
    }

    public String getDescription() {
        return "This is a wrapper class for MyFinalClass";
    }
}

可以看到,在 MyWrapperClass 中使用了 MyFinalClass 的实例,并添加了一个 getDescription() 方法来获取包装器类的描述信息。

然后在其他类中,可以使用 MyWrapperClass 来访问 MyFinalClass 的实例,并使用包装器类的额外方法或属性:

MyFinalClass finalInstance = new MyFinalClass(10);
MyWrapperClass wrapperInstance = new MyWrapperClass(finalInstance);
int value = wrapperInstance.getValue(); // 10
String description = wrapperInstance.getDescription(); // "This is a wrapper class for MyFinalClass"

通过这种方式,可以在不继承 MyFinalClass 的情况下,使用包装器类来拓展 MyFinalClass 的功能。

4.final方法可以被重载吗?

是的,final修饰的方法可以被重载,但不能被子类重写。

当子类中声明了一个和父类中同名、同参数列表、同返回类型的方法时,这个方法被称为重载方法。重载方法可以在子类中提供新的实现,但是不能改变父类中 final 方法的行为。

以下是一个示例,其中父类中有一个 final 方法 printMessage(),子类中定义了一个重载方法 printMessage(String message)

public class Parent {
    public final void printMessage() {
        System.out.println("Hello, world!");
    }
}

public class Child extends Parent {
    public void printMessage(String message) {
        System.out.println("Hello, " + message + "!");
    }
}

在上面的例子中,Child 类中的 printMessage(String message) 方法是一个重载方法,不会覆盖父类中的 printMessage() 方法。如果尝试在子类中定义一个与父类中的 printMessage() 方法相同的方法,则会编译错误。

5.说说基本类型的final域重排序规则?

在Java语言中,对于基本类型的final域,JVM会禁止对它们的重排序,确保在对象发布之后,该域的值对所有线程可见,因此满足了可见性和有序性。

具体来说,对于final域的写入,JVM会在构造函数执行期间将其初始值写入该域,并将构造函数的return指令之前的所有写操作刷新到主内存中,这样,其他线程在获取该域的值时,就能读到这个最终的值。

同时,由于JVM不允许对final域进行重排序,因此在使用final域时,程序员也不需要担心其可能被重排序从而破坏线程安全。

6.说说final的原理?

在Java中,final关键字可以用来修饰变量、方法和类。对于变量,final保证了变量只被赋值一次,也就是说,final变量的值在赋值后不可更改。这个特性可以通过内存屏障来实现。在写final变量的时候,JVM会在写操作之前插入一个StoreStore屏障,保证该写操作不会被重排到屏障之后。在读final变量的时候,JVM会在读操作之后插入一个LoadLoad屏障,保证该读操作不会被重排到屏障之前。

对于方法和类,final关键字可以用来禁止子类重写方法和继承类。这个特性可以通过在编译期生成类文件时进行优化实现。如果一个类被声明为final,那么编译器会在编译时对该类的所有方法进行静态绑定(static binding),也就是在编译期就确定方法的调用方式,而不需要在运行时进行动态绑定(dynamic binding)。这样,就可以避免在运行时进行虚方法表(virtual method table)的查找,提高程序的执行效率。

总的来说,final的原理是通过内存屏障和编译器优化来实现的,保证了final变量的值只被赋值一次,以及final方法和类的不可更改性和继承性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值