Java笔试面试题AI答之线程(5)

发布于:2024-08-23 ⋅ 阅读:(25) ⋅ 点赞:(0)

25. 为什么我们调用start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法?

在Java中,当我们创建一个线程时,通常会通过实现Runnable接口或者继承Thread类的方式来实现。无论是哪种方式,最终都需要覆盖(Override)run()方法,以定义线程的执行体。而启动线程则是通过调用线程的start()方法来实现的。

为什么调用start()方法时会执行run()方法?

这是因为start()方法的内部实现中,会通过调用操作系统的底层API来创建一个新的线程,并且在这个新线程中调用run()方法。这意味着run()方法的内容将会在新的线程上下文中执行,而不是在调用start()方法的线程中执行。

为什么不能直接调用run()方法?

如果直接调用run()方法,那么run()方法的内容将会在调用它的线程(通常是主线程)中同步执行,而不是在新的线程中异步执行。这样做就失去了多线程编程的意义,因为并没有实现真正的并发执行。

总结来说,start()方法和run()方法的设计是为了实现多线程编程的。start()方法用于启动一个新线程,而run()方法则定义了这个新线程的执行内容。直接调用run()方法则无法实现多线程的效果。

26. 简述Synchronized的原理 ?

Synchronized是Java中的一个关键字,用于实现线程之间的同步,确保共享资源在多线程环境下的安全访问。其原理主要基于以下几个方面:

1. 监视器锁(Monitor Lock)

  • 原理:synchronized通过对象级别的锁(也称为监视器锁)来实现同步。每个Java对象都有一个与之关联的监视器锁。当线程进入被synchronized修饰的代码块时,它会尝试获取该对象的监视器锁。如果锁已被其他线程持有,则当前线程将被阻塞,直到获取到锁为止。一旦线程执行完synchronized代码块中的代码,它会释放监视器锁,以便其他线程可以获取锁并执行。
  • 应用:无论是synchronized修饰的方法还是代码块,其本质都是通过获取和释放对象的监视器锁来实现同步。

2. 锁的升级

  • 背景:在Java 6及以后的版本中,为了优化synchronized的性能,JVM引入了锁升级的机制。锁一共有四种状态(由低到高的次序):无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的等级只可以升级,不可以降级。
  • 过程
    1. 无锁状态:对象刚开始时处于无锁状态,即没有任何线程持有该对象的锁。
    2. 偏向锁:为了减少无竞争情况下的锁开销,JVM引入了偏向锁。当一个线程首次访问同步代码块时,它会在对象头和当前线程的栈帧中记录偏向的线程ID。这样,在后续的执行中,如果仍然是同一个线程访问该同步代码块,JVM就可以判断出来,并允许该线程无锁地执行同步代码。偏向锁实际上是一种延迟加锁的机制,旨在消除无竞争情况下的同步原语,提高程序的运行性能。
    3. 轻量级锁:当偏向锁撤销后,或者多个线程交替执行同步代码块时,锁会升级为轻量级锁。轻量级锁的加锁过程是通过CAS操作实现的,它试图将对象头的Mark Word替换为指向线程栈帧中锁记录的指针。如果成功,则当前线程获得锁;如果失败,说明存在竞争,此时会尝试自旋等待(即让当前线程空转一段时间),然后再次尝试获取锁。如果自旋等待达到一定的次数仍然没有获取到锁,轻量级锁就会升级为重量级锁。
    4. 重量级锁:重量级锁是Java中最基础的锁机制,它的实现依赖于操作系统的互斥量(Mutex)。当轻量级锁无法满足性能需求时,会升级为重量级锁。此时,未获取到锁的线程会被阻塞,并进入等待状态,直到持有锁的线程释放锁。重量级锁的性能开销相对较大,因为它涉及到用户态和内核态的切换。

3. synchronized的特性

  • 原子性:确保一个操作或多个操作要么全部执行完成,要么全部不执行。
  • 可见性:通过内存屏障(Memory Barrier)保证一个线程对共享变量的修改对其他线程是可见的。
  • 有序性:禁止指令重排,保证程序执行的顺序按照代码的先后顺序执行。

4. 使用注意事项

  • synchronized的范围应该尽量小,避免锁住过多的代码,以提高程序的并发性能。
  • 避免在synchronized代码块内部进行耗时的操作,这可能会导致其他线程长时间等待。
  • 当使用synchronized关键字时,要确保所有访问共享资源的方法都被同步,否则仍然可能出现线程安全问题。

综上所述,synchronized的原理是通过对象级别的锁(监视器锁)和锁升级机制来实现线程之间的同步,确保共享资源的安全访问。同时,它还具有原子性、可见性和有序性等特性,是Java并发编程中不可或缺的一部分。

27. 解释为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

wait(), notify(), 和 notifyAll() 是 Java 中用于线程间通信的三个重要方法,它们都属于 Object 类的方法。这三个方法必须在同步方法或同步块中被调用,这主要是出于以下几个原因:

  1. 确保线程安全
    wait(), notify(), 和 notifyAll() 方法设计用于在多线程环境中协调线程之间的操作。这些操作通常涉及对共享资源的访问和修改。由于这些操作对共享资源有直接影响,因此必须确保在访问这些资源时线程是安全的。通过将 wait(), notify(), 和 notifyAll() 调用放在同步方法或同步块中,可以确保在同一时刻只有一个线程能够执行这些操作,从而避免了数据竞争和条件竞争等并发问题。

  2. 锁定机制
    在 Java 中,每个对象都有一个与之关联的锁(也称为监视器锁)。当线程进入同步方法或同步块时,它会自动获取该对象的锁。这个锁保证了在同一时刻只有一个线程能够执行同步代码块中的代码。wait(), notify(), 和 notifyAll() 方法依赖于这个锁机制来工作。具体来说,wait() 方法会使当前线程释放锁并进入等待状态,直到其他线程调用同一对象的 notify()notifyAll() 方法来唤醒它。如果这些方法不是在同步方法或同步块中被调用,那么它们就没有与之关联的锁,这将导致无法正确管理线程之间的通信和同步。

  3. 避免死锁和活锁
    wait(), notify(), 和 notifyAll() 调用放在同步方法或同步块中还有助于避免死锁和活锁等并发问题。死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况,而活锁则是指线程之间不断尝试获取资源但总是因为某些原因而失败,导致无法向前推进。通过将这些操作限制在同步块中,可以确保线程在尝试获取资源或进行通信时遵循一致的规则和顺序,从而减少了死锁和活锁的风险。

综上所述,wait(), notify(), 和 notifyAll() 方法必须在同步方法或同步块中被调用,以确保线程安全、利用锁定机制来管理线程之间的通信和同步,并避免并发问题如死锁和活锁的发生。

28. Java 如何实现多线程之间的通讯和协作?

在Java中,实现多线程之间的通讯和协作可以通过多种方式来完成,主要包括以下几种机制:

1. 使用共享变量

这是最基本的方式,多个线程可以访问同一个共享变量来实现数据的交换。但是,这种方式需要特别注意线程安全问题,通常需要使用synchronized关键字或java.util.concurrent包中的工具类来确保数据的一致性和完整性。

2. 使用wait()和notify()/notifyAll()

这两个方法定义在Object类中,因此任何对象都可以作为锁对象。当线程需要等待某个条件成立时,可以调用该对象的wait()方法进入等待状态,并释放锁。其他线程在修改条件后,可以调用该对象的notify()notifyAll()方法来唤醒一个或所有等待的线程。注意,这两个方法必须在同步方法或同步块中调用。

3. 使用join()

join()方法是Thread类的一个方法,用于等待线程终止。如果在一个线程A中调用另一个线程B的join()方法,那么线程A会暂停执行,直到线程B执行完毕。这可以用于控制线程的执行顺序。

4. 使用Lock和Condition

java.util.concurrent.locks包中的Lock接口提供了比synchronized方法和语句更广泛的锁定操作。Lock接口允许更灵活的结构,可以具有完全不同的属性,并且可以支持多个相关的Condition对象。Condition对象提供了与Object监视器方法(waitnotifynotifyAll)功能相对应的方法,但允许每个锁有多个等待集。

5. 使用BlockingQueue

java.util.concurrent包中的BlockingQueue接口及其实现类(如ArrayBlockingQueueLinkedBlockingQueue等)提供了线程安全的队列实现,用于在多线程之间进行数据交换。生产者线程可以将数据放入队列中,而消费者线程可以从队列中取出数据。这种方式特别适用于生产者-消费者场景。

6. 使用Semaphore

Semaphore(信号量)是一个计数信号量,用于控制同时访问某个特定资源或资源池的操作数量,或者同时执行某个指定操作的数量。它还可以用来实现生产者-消费者问题中的同步机制。

7. 使用CyclicBarrier和CountDownLatch

  • CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及固定大小的线程组,并且这些线程必须相互等待,直到它们都达到某个公共屏障点,才能继续执行时,可以使用CyclicBarrier
  • CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

8. 使用Exchanger

Exchanger类可用于两个线程之间交换信息。当两个线程都到达某个同步点时,它们交换各自的数据,然后继续执行。这可以用于遗传算法、管道设计或任务交换等场景。

这些机制各有特点,可以根据具体的应用场景和需求来选择合适的方式来实现多线程之间的通讯和协作。

29. Thread 类中的 yield 方法有什么作用?

Thread 类中的 yield() 方法是 Java 中的一个静态方法,用于提示当前正在运行的线程暂停执行,以便让其他线程有机会执行。但是,需要注意的是,yield() 方法并不会导致线程进入等待、睡眠或阻塞状态。它仅仅是一个提示,告诉 JVM(Java 虚拟机)当前线程愿意放弃当前的处理器时间,让其他线程有机会运行。

然而,yield() 方法的具体行为取决于 JVM 的实现。不同的 JVM 可能会以不同的方式响应 yield() 调用。有些 JVM 可能会忽略这个提示,继续执行当前线程;而有些 JVM 则可能会将当前线程置于就绪状态,让调度器重新选择线程执行。但无论如何,yield() 方法都不能保证其他线程会立即运行或者当前线程会立即停止执行。

使用 yield() 方法的目的通常是为了提高程序的并发性和响应性,尤其是在线程执行时间非常短或者线程间需要频繁交替执行时。然而,在大多数情况下,仅仅依赖 yield() 方法来控制线程的执行顺序或优先级是不可靠的。如果需要更精确地控制线程的执行顺序或优先级,应该考虑使用其他同步机制,如 synchronized 关键字、Lock 接口、SemaphoreCountDownLatchCyclicBarrier 等。

此外,需要注意的是,过度使用 yield() 方法可能会导致程序性能下降,因为它会增加线程调度的开销。因此,在决定使用 yield() 方法之前,应该仔细评估其对程序性能的影响。

30. 为什么说 Synchronized 是非公平锁?

Synchronized在Java中是一种内置的锁机制,用于保护共享资源的并发访问,确保同一时刻只有一个线程可以获取到锁,从而避免多个线程同时修改共享资源的问题。然而,Synchronized被归类为非公平锁,这主要基于以下几点原因:

非公平锁的定义

非公平锁是指多个线程获取锁的顺序是不确定的,不按照申请锁的顺序来排队。一个线程在等待锁时,不管自己是不是在等待队列的头部,都有机会在其他线程释放锁后立即获取锁。这种锁机制可能会导致某些线程长时间地无法获取到锁,产生饥饿现象。

Synchronized的非公平性表现

在Synchronized的实现中,当多个线程同时请求同一个锁时,这些线程会被放入一个等待队列中。然而,当锁被释放时,JVM并不是按照线程请求锁的顺序来选择线程获取锁,而是随机选择一个线程。这就意味着,一个刚进入等待队列的线程有可能比已经在队列中等待很久的线程更快地获取到锁。

性能考虑

Synchronized设计为非公平锁主要是为了提高系统的吞吐量。在并发编程中,锁的公平性和性能之间往往存在权衡。公平锁虽然能够确保每个线程都按照申请锁的顺序来获取锁,但可能会因为线程需要等待较长时间而导致性能下降。非公平锁则允许线程插队获取锁,从而提高了整体的吞吐量,尽管这可能会牺牲一些公平性。

实际应用

在实际应用中,是否使用公平锁还是非公平锁取决于具体的需求和场景。如果系统对公平性有较高要求,比如需要避免某些线程长时间无法获取到锁的情况,那么可以考虑使用公平锁(如Java中的ReentrantLock可以通过构造函数指定为公平锁)。然而,如果系统更注重性能,且可以容忍一定程度的线程饥饿现象,那么Synchronized提供的非公平锁机制可能是一个更好的选择。

总结

综上所述,Synchronized被归类为非公平锁主要是因为它在释放锁时并不是按照线程请求锁的顺序来选择线程获取锁,而是随机选择一个线程。这种设计虽然可能导致某些线程长时间无法获取到锁,但提高了系统的吞吐量。在实际应用中,应根据具体需求和场景来选择使用公平锁还是非公平锁。

答案来自文心一言,仅供参考


网站公告

今日签到

点亮在社区的每一天
去签到