【Java】Java核心要点总结:59

简介: 1. 线程的run()和start()有什么区别,为什么不直接调用run()Java中通过继承 Thread 或实现 Runnable 接口来创建线程,线程是通过 start() 方法启动的,而不是直接调用 run() 方法。下面是它们之间的区别:start() 和 run() 的区别调用 start() 方法会启动一个新线程并执行其中的 run() 方法,而直接调用 run() 方法将在当前线程中执行 run() 方法,并不会创建新的线程。

1. 线程的run()和start()有什么区别,为什么不直接调用run()


Java中通过继承 Thread 或实现 Runnable 接口来创建线程,线程是通过 start() 方法启动的,而不是直接调用 run() 方法。下面是它们之间的区别:


start() 和 run() 的区别

调用 start() 方法会启动一个新线程并执行其中的 run() 方法,而直接调用 run() 方法将在当前线程中执行 run() 方法,并不会创建新的线程。


为什么不直接调用 run()

如果直接调用 run() 方法,则该方法就会在当前线程中执行,而不会创建新的线程,这样就失去了多线程的优势。由于 Java 是单继承机制,如果某个类已经继承自其他类,则无法再继承自 Thread 类。因此,通常情况下我们更倾向于实现 Runnable 接口来创建线程,这样可以避免单继承带来的限制。


同时,在使用多线程编程时,我们需要控制线程之间的共享资源,保证线程安全。如果我们直接调用 run() 方法,那么所有线程都是在同一个主线程上运行,共享同一个堆空间和栈空间,容易出现数据竞争和线程安全问题,降低系统的稳定性。


因此,我们应该始终使用 start() 方法来启动一个新线程,而不是直接调用 run() 方法。


2. synchronized是什么,以及原理



synchronized 是 Java 中的一种同步机制,它可以保证在多线程并发执行时共享数据的安全性。


synchronized 关键字可以用于方法或者代码块上,如果使用在代码块上,需要指定一个对象作为锁,该对象可以是任意的 Object 对象。当某个线程要执行 synchronized 方法或者代码块时,必须先获得该锁才能执行,如果其他线程已经获取了锁,那么当前线程就只能等待锁的释放。在方法或者代码块执行完成后,当前线程会自动释放锁。


synchronized 的原理是基于监视器锁(monitor),每个对象内部都存在一个监视器锁(也称为管程),通过这个锁来实现对对象的互斥访问。当线程进入 synchronized 代码块时,线程会尝试获取对象的监视器锁,如果获取到了锁,则说明其他线程没有占用对象资源,当前线程就可以进入临界区然后执行代码;如果无法获取到锁,那么线程就会被阻塞,直到持有锁的线程释放了锁。此外,Java 中的 synchronized 还具有可见性和禁止指令重排序的特性,保证了 volatile 变量的安全性。


synchronized 是通过加锁的方式来实现线程之间的同步,保证了共享变量的可见性、原子性和有序性,避免了多个线程对共享数据产生竞争的问题,保证了多线程程序的正确性。

3. Java中如何实现多线程的通讯和协作


在 Java 中,多线程通信和协作可以使用以下几种方式:


wait()、notify() 和 notifyAll() 方法

这三个方法是在 Object 类中定义的,wait() 是让当前线程等待,直到其他线程调用 notify() 或 notifyAll() 方法才能唤醒,notify() 则是随机选择其中一个等待线程进行通知,notifyAll() 则会通知所有等待线程继续执行。使用这些方法时必须要先获得对象的锁,也就是必须在 synchronized 代码块中使用。


join() 方法

join() 方法让当前线程等待另一个线程执行完后再继续执行,其实现原理也是调用了 wait() 方法。join() 方法通常用于让主线程等待子线程执行完成后再执行,或者等待一组线程全部执行完毕再进行下一步操作。


sleep() 方法

sleep() 方法让当前线程暂停一段时间,以便其他线程有机会执行,但不释放锁。sleep() 方法常用于模拟耗时操作,例如网络请求和计算密集型任务,避免浪费 CPU 资源。


Lock 和 Condition 接口

Java 5 引入了 Lock 和 Condition 接口,它们提供了一种更灵活的并发编程方案,Lock 接口提供了与 synchronized 同样的功能,Condition 接口则相当于 wait() 和 notify() 方法的组合,可以更精细地控制线程间通信和协作。


在实现多线程通讯和协作时,我们需要根据具体情况选择不同的方式,在不同场景下使用合适的方法可以提高程序的效率和稳定性。

4. Volatile有什么特点,为什么能够保证变量的可见性


Volatile 是一个 Java 关键字,用于修饰变量,具有以下特点:


可见性:在一个线程中对 volatile 变量的修改会立即刷新到主内存中,并通知其他线程该变量的值已经被修改,其他线程通过读取该变量时可以获取最新的值。


禁止指令重排序:使用 volatile 修饰的变量赋值后不能保证执行顺序,但能够保证前面的操作一定先于后面的操作执行。也就是说,volatile 变量在赋值后,该语句之前的所有读写操作都完成了,该语句之后的所有读写操作还未进行。


不具有原子性: volatile 并不能保证复合操作的原子性,例如 num++,虽然用 volatile 修饰了 num,但是多个线程同时对它进行自增操作时不能保证结果的正确性。


通过上述特点,可以发现 volatile 能够保证变量的可见性是因为它能够禁止 CPU 和编译器对代码重排,将修改后的值立即刷回主内存,而其他线程读取该变量时必须从内存中获取最新的值,从而保证了可见性。


需要注意的是,虽然 volatile 能够保证变量的可见性和禁止指令重排序,但并不能完全解决并发问题,在一些复合操作或者需要原子性保证的操作中,还需要使用其它的同步机制,例如 synchronized、Lock 和 Atomic 类等。

5. 为什么说synchronized是一个悲观锁,乐观锁的实现原理是什么,什么是CAS,它有什么特性


Synchronized 是一种悲观锁,因为它假定代码段中的多线程竞争非常激烈,所以每个线程都会尝试获得锁。然而,在实际运行过程中,并非所有的代码段都会产生高强度竞争,如果使用 synchronized 占用了锁,而实际上还没有其他线程在竞争该资源,这样就会造成效率的浪费。


乐观锁是另外一种锁的思路,其核心思想是假设并发情况下操作不会出现冲突,即先进行操作,在更新前后比较,如果计算机中值没被别的线程修改,则更新成功;如果值已经被其他线程更新,则需要重试。因此,对于使用乐观锁机制的代码,在低并发的情况下性能较好,但是在高并发的情况下重试次数会增加,导致性能下降。


乐观锁的实现原理可以通过 CAS(Compare and Swap)指令来实现。CAS 是一种无锁算法,它利用处理器提供的原子操作指令,保证了操作的原子性和可串行性。CAS 操作将内存中某个位置的值与一个预期值进行比较,如果相等,那么执行操作,否则啥也不干。因为 CAS 靠的是硬件支持,所以它执行非常快,并且很少有竞争失败的情况。在 Java 中,Atomic 类和 AtomicReference 类就是利用了 CAS 的特性来实现乐观锁。


要点总结:


synchronized 是一种悲观锁,乐观锁采用先操作再比较的策略。

乐观锁的性能对并发量和重试次数敏感,适合低竞争代码,不适合高竞争代码。

CAS 是一种无锁算法,保证操作的原子性和可串行性,适用于乐观锁机制的实现。

值得注意的是,在编写并发代码时需要评估功能需求、应用场景和性能等多个方面,选择合适的锁策略和实现机制,进行性能优化和效果提升。

相关文章
|
安全 Java 数据库连接
【Java】Java核心要点总结:62
1. 线程中的线程是怎么创建的,是一开始就随着线程池的启动创建好的吗? 线程中的线程通常是通过在父线程中创建新的Thread对象并将其加入线程池中实现的。当然,这个过程也可以通过一些其他的方式来实现,比如使用ExecutorService.submit()方法提交一个Callable或Runnable任务。
|
缓存 安全 Java
【Java】Java核心要点总结70
1. volatile 如何保证变量的可⻅性? 在Java中,使用volatile关键字可以确保变量的可见性。 当一个线程修改了一个被volatile修饰的变量时,它会立即将该变量的最新值刷新到主内存。而其他线程在读取该变量时,会从主内存中重新获取最新值,而不是使用缓存中的旧值。 这样做的原因是,普通的变量在多线程环境下存在线程间不可见的问题。每个线程都有自己的工作内存,由于运行速度快和编译优化等原因,线程可能会直接读取工作内存中的旧值,而不去主内存中获取最新的值,导致线程之间的数据不一致。
|
3月前
|
消息中间件 NoSQL Java
Java知识要点及面试题
该文档涵盖Java后端开发的关键知识点,包括Java基础、JVM、多线程、MySQL、Redis、Spring框架、Spring Cloud、Kafka及分布式系统设计。针对每个主题,文档列举了重要概念及面试常问问题,帮助读者全面掌握相关技术并准备面试。例如,Java基础部分涉及面向对象编程、数据类型、异常处理等;JVM部分则讲解内存结构、类加载机制及垃圾回收算法。此外,还介绍了多线程的生命周期、同步机制及线程池使用,数据库设计与优化,以及分布式系统中的微服务、RPC调用和负载均衡等。
|
4月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。
|
4月前
|
Java
【Java基础面试十六】、Java中的多态是怎么实现的?
这篇文章解释了Java中多态的实现机制,主要是通过继承,允许将子类实例赋给父类引用,并在运行时表现出子类的行为特征,实现这一过程通常涉及普通类、抽象类或接口的使用。
|
缓存 监控 Java
【Java】Java核心要点总结:61
1. java中的线程池是如何实现的 Java 中的线程池是通过 ThreadPoolExecutor 类实现的。ThreadPoolExecutor 继承自 AbstractExecutorService,并实现了 Executor、ExecutorService 和 Future 接口,它可以为程序提供管理和控制多个工作者线程的功能,提高线程重用性并避免线程频繁创建销毁的开销。
|
监控 安全 Java
Java面试题-Java核心基础
Java面试题-Java核心基础
106 2
【Java】Java核心要点总结 68
1. 为什么重写 equals() 时候必须重写 hashCode() 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
|
存储 Java C语言
【Java】Java核心要点总结 67
1. 浮点数运运算会有精度损失 这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
|
存储 Java 程序员
【Java】Java核心要点总结 69
1. BIO NIO AIO 在Java中,BIO、NIO和AIO是针对网络编程的不同I/O模型: BIO(Blocking I/O):传统的阻塞式I/O模型,它以阻塞的方式进行数据读写操作。当一个线程执行I/O操作时,会被阻塞,直到数据准备好或者操作完成。这种模型相对简单,但对并发处理能力较弱。 NIO(Non-blocking I/O):非阻塞式I/O模型,引入了选择器(Selector)和通道(Channel)的概念。使用NIO,可以通过一个线程处理多个通道的I/O操作,提升了并发处理能力。但需要手动检查是否有数据可用,必要时才进行读写操作。
下一篇
DataWorks