【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修饰的变量时,它会立即将该变量的最新值刷新到主内存。而其他线程在读取该变量时,会从主内存中重新获取最新值,而不是使用缓存中的旧值。 这样做的原因是,普通的变量在多线程环境下存在线程间不可见的问题。每个线程都有自己的工作内存,由于运行速度快和编译优化等原因,线程可能会直接读取工作内存中的旧值,而不去主内存中获取最新的值,导致线程之间的数据不一致。
|
2月前
|
消息中间件 NoSQL Java
Java知识要点及面试题
该文档涵盖Java后端开发的关键知识点,包括Java基础、JVM、多线程、MySQL、Redis、Spring框架、Spring Cloud、Kafka及分布式系统设计。针对每个主题,文档列举了重要概念及面试常问问题,帮助读者全面掌握相关技术并准备面试。例如,Java基础部分涉及面向对象编程、数据类型、异常处理等;JVM部分则讲解内存结构、类加载机制及垃圾回收算法。此外,还介绍了多线程的生命周期、同步机制及线程池使用,数据库设计与优化,以及分布式系统中的微服务、RPC调用和负载均衡等。
|
6月前
|
存储 自然语言处理 Java
Java基础之计算机基础知识引出Java
计算机基础知识概览:探讨计算机科学的约定性,指出其底层基于二进制逻辑运算,由晶体管开关控制。二进制与十进制间的转换是基础,计算机通过ASCII编码处理英文字符,但不包含汉字。早期计算机发展始于西方,从巴贝奇的差分机到图灵机,再到冯·诺伊曼结构。Unicode标准解决多语言字符编码,如UTF-8和UTF-16。编程语言分为低级(如汇编)和高级(如Java),以适应不同需求。
21 0
|
缓存 监控 Java
【Java】Java核心要点总结:61
1. java中的线程池是如何实现的 Java 中的线程池是通过 ThreadPoolExecutor 类实现的。ThreadPoolExecutor 继承自 AbstractExecutorService,并实现了 Executor、ExecutorService 和 Future 接口,它可以为程序提供管理和控制多个工作者线程的功能,提高线程重用性并避免线程频繁创建销毁的开销。
|
监控 安全 Java
Java面试题-Java核心基础
Java面试题-Java核心基础
104 2
|
安全 Java API
【Java】Java核心要点总结:60
1. 乐观锁一定就是好的吗 乐观锁并不一定好,它有自己的适用场景和局限性。 乐观锁的优点在于在低并发环境下表现良好,操作基本上都能够成功,不会阻塞其他线程的执行。此外,乐观锁没有锁带来的资源竞争和多线程间的上下文切换开销,对于高并发的场景下可以提供更好的性能表现。 但是,在高并发场景下乐观锁的表现往往受到影响,因为多个线程同时执行操作和检查比较,会出现重试次数增加的情况,进而增加了延迟和开销,降低了系统的吞吐量和性能。此时,悲观锁可能比乐观锁更适合解决问题,因为它能够阻塞其他线程,等待当前线程释放锁后再执行。
|
存储 Java C语言
【Java】Java核心要点总结 67
1. 浮点数运运算会有精度损失 这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
|
存储 缓存 Java
【Java】Java核心要点总结 66
1. 成员变量 和 局部变量 的区别 ● 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 ● 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
【Java】Java核心要点总结 68
1. 为什么重写 equals() 时候必须重写 hashCode() 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。