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 是一种无锁算法,保证操作的原子性和可串行性,适用于乐观锁机制的实现。
值得注意的是,在编写并发代码时需要评估功能需求、应用场景和性能等多个方面,选择合适的锁策略和实现机制,进行性能优化和效果提升。