1.volatile如何和cas结合保证原子性
在多线程编程中,要保证某个操作的原子性(Atomicity),即在并发执行的情况下,这个操作要么完全执行成功,要么完全不执行。volatile
关键字可以用于确保变量的可见性,而CAS
(Compare and Swap)是一种原子操作,可以实现对变量的原子更新。
要结合volatile
和CAS
来保证操作的原子性,可以使用以下步骤:
-
将需要保证原子性的变量声明为
volatile
类型。这样可以确保每次访问该变量时,都会从主内存中读取最新的值,而不是使用线程的本地缓存。 -
使用
CAS
操作来进行原子更新。CAS
操作由三个参数组成:需要更新的变量、期望的值和新值。它会先比较变量的当前值是否与期望的值相等,如果相等,则将变量的值更新为新值;如果不相等,则不进行更新。CAS
操作是原子的,因此可以确保只有一个线程能够成功更新变量的值。
下面是一个简单的示例代码,演示了如何使用volatile
和CAS
结合来保证操作的原子性:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private volatile int counter = 0;
private AtomicInteger atomicCounter = new AtomicInteger(0);
public void increment() {
// 使用volatile变量进行操作
counter++;
}
public void atomicIncrement() {
// 使用CAS操作进行原子更新
atomicCounter.getAndIncrement();
}
}
在上面的代码中,increment()
方法使用volatile
变量counter
来进行操作,但是由于不是原子操作,可能会存在线程安全问题。而atomicIncrement()
方法使用AtomicInteger
类提供的原子操作来保证更新的原子性。
需要注意的是,volatile
关键字只能保证变量的可见性,无法保证复合操作的原子性。如果需要进行多个操作的原子执行,可以使用Atomic
类提供的原子操作,或者使用锁机制(如synchronized
关键字或Lock
接口)来保证原子性。
2.新建 T1、T2、T3 三个线程,如何使用join方法保证它们按顺序执行
public class ThreadOrderExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable("T1"));
Thread t2 = new Thread(new MyRunnable("T2"));
Thread t3 = new Thread(new MyRunnable("T3"));
try {
// 启动线程T1
t1.start();
// 等待线程T1执行完成
t1.join();
// 启动线程T2
t2.start();
// 等待线程T2执行完成
t2.join();
// 启动线程T3
t3.start();
// 等待线程T3执行完成
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 所有线程执行完成后,主线程继续执行其他操作
System.out.println("All threads have completed.");
}
static class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Thread " + name + " is executing.");
}
}
}
3.怎么控制同一时间只有 3 个线程运行 用semaphore
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // 初始化信号量为3
// 创建并启动10个线程
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(new MyRunnable(i, semaphore));
thread.start();
}
}
static class MyRunnable implements Runnable {
private int id;
private Semaphore semaphore;
public MyRunnable(int id, Semaphore semaphore) {
this.id = id;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire(); // 获取信号量,如果没有可用的许可证,则阻塞线程
// 线程执行任务
System.out.println("Thread " + id + " is running.");
Thread.sleep(2000); // 模拟线程执行一段时间
System.out.println("Thread " + id + " is done.");
semaphore.release(); // 释放信号量的许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.如何检测一个线程是否带有锁 java.lang.Thread#holdsLock 方法
public class ThreadLockDetection {
public static void main(String[] args) {
Object lock = new Object();
// 线程1获取锁
Thread thread1 = new Thread(() -> {
synchronized (lock) {
// 执行同步代码块
System.out.println("Thread 1 acquired the lock");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 线程2尝试获取锁
Thread thread2 = new Thread(() -> {
synchronized (lock) {
// 执行同步代码块
System.out.println("Thread 2 acquired the lock");
}
});
thread1.start();
thread2.start();
// 检测线程1是否持有锁
boolean thread1HoldsLock = Thread.holdsLock(lock);
System.out.println("Thread 1 holds the lock: " + thread1HoldsLock);
// 检测线程2是否持有锁
boolean thread2HoldsLock = Thread.holdsLock(lock);
System.out.println("Thread 2 holds the lock: " + thread2HoldsLock);
}
}
5.线程同步
线程同步是一种多线程编程的技术,用于控制多个线程对共享资源的访问,以避免竞争条件和数据不一致性的问题。在Java中,常用的线程同步机制有以下几种:
-
synchronized关键字:synchronized关键字可以修饰代码块或方法,它使用内置锁(也称为监视器锁)来实现同步。同一时刻只允许一个线程进入同步代码块或方法,其他线程需要等待锁释放才能执行。
synchronized (lockObject) {
// 同步代码块
}
public synchronized void synchronizedMethod() {
// 同步方法
}
2.ReentrantLock类:ReentrantLock是Java提供的可重入锁实现类。它提供了与synchronized类似的功能,但具有更高的灵活性和可扩展性。可以使用lock()方法获取锁,使用unlock()方法释放锁。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 同步代码块
} finally {
lock.unlock();
}
3.ReadWriteLock接口:ReadWriteLock接口提供了读写锁的机制,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。ReadWriteLock接口的实现类是ReentrantReadWriteLock。
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
4.AtomicInteger类:AtomicInteger是一个原子类,提供了原子操作的整型变量。它通过CAS(Compare and Swap)操作实现原子更新,避免了线程安全问题。
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // 原子递增操作
6.Fork/Join 框架
Fork/Join框架是Java中的一个并发编程框架,用于处理递归的、可拆分的任务。它的设计目标是使并行任务的编写更加容易,同时充分利用多核处理器的优势。
Fork/Join框架的核心概念是"分而治之"(Divide and Conquer)策略。它将一个大任务拆分成多个小任务,并使用递归的方式处理这些小任务。当小任务足够简单时,可以直接执行,否则会进一步拆分成更小的子任务。一旦所有子任务都完成,结果将合并为最终结果。
Fork/Join框架的关键组件有:
1. ForkJoinPool:是一个线程池,用于执行Fork/Join任务。它管理着工作线程的集合,并提供任务调度和工作线程的管理。
2. ForkJoinTask:是一个表示可执行任务的抽象类。它有两个重要的子类:RecursiveAction(没有返回值的任务)和RecursiveTask(带有返回值的任务)。用户可以继承这些子类,实现自定义的任务。
3. ForkJoinWorkerThread:是Fork/Join框架中的工作线程。每个工作线程都绑定到ForkJoinPool,并负责执行分配给它的任务。
Fork/Join框架的使用步骤通常包括:
1. 继承`RecursiveAction`或`RecursiveTask`,实现自定义的任务。
2. 在任务类中,重写`compute()`方法来定义任务的执行逻辑。在`compute()`方法中,根据需要拆分任务,并使用`fork()`方法提交子任务。
3. 使用`join()`方法等待子任务的完成,然后合并子任务的结果。
4. 创建`ForkJoinPool`对象,并将任务提交给线程池执行。
Fork/Join框架可以在处理递归任务时提供高效的并行计算能力,特别适用于处理复杂的问题。它可以自动管理任务的拆分、调度和结果合并,简化了并行任务的编写过程,并发挥多核处理器的潜力,提高了程序的性能和效率。
7.自旋锁
自旋锁(Spin Lock)是一种基于忙等待的锁机制,它不会使线程进入阻塞状态,而是在获取锁时反复检查锁的状态,直到成功获取锁或达到一定的等待次数。
自旋锁的特点是:
- 线程在尝试获取锁时,通过循环不断检查锁的状态。
- 如果锁被其他线程占用,当前线程将忙等待(自旋)直到锁被释放。
- 自旋锁适用于短时间的争用情况,当锁的持有者很快就会释放锁时,自旋等待可以避免线程进入阻塞状态,从而减少线程上下文切换的开销。
自旋锁的优点是:
- 自旋锁避免了线程切换的开销,因为线程不需要被挂起和恢复。
- 在锁的竞争情况下,自旋锁能够快速尝试获取锁,减少了线程进入阻塞状态的等待时间。
然而,自旋锁也存在一些限制和问题:
- 自旋锁不适用于长时间的争用情况。如果持有锁的线程长时间不释放,那些自旋等待的线程会一直忙等,浪费CPU资源。
- 自旋锁可能导致死锁。如果多个线程同时持有自旋锁,并且相互等待对方释放锁,就会出现死锁情况。
在Java中,`java.util.concurrent.atomic`包提供了一些基于自旋锁的原子类,例如`AtomicInteger`、`AtomicBoolean`等。此外,Java中的`ReentrantLock`也可以使用自旋锁来实现,通过构造函数参数`fair`来指定是否使用公平锁,默认是非公平锁。
8.Runnable 和 Thread 用哪个好?
在Java中,有两种常见的方式来创建和执行多线程任务:使用Runnable
接口和继承Thread
类。
-
使用
Runnable
接口:- 实现
Runnable
接口,并重写run()
方法来定义线程的任务逻辑。 - 创建
Thread
对象,将实现了Runnable
接口的对象作为参数传递给Thread
构造函数。 - 调用
Thread
对象的start()
方法启动线程。 - 这种方式将任务逻辑和线程对象解耦,使代码更具可重用性和灵活性。
- 实现
public class MyThread extends Thread {
@Override
public void run() {
// 线程任务逻辑
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2. 继承Thread
类:
- 创建一个继承自
Thread
类的子类,并重写run()
方法来定义线程的任务逻辑。 - 直接创建子类的对象,并调用对象的
start()
方法启动线程。 - 这种方式将任务逻辑和线程对象合并在一起,相对简单直接。
public class MyThread extends Thread {
@Override
public void run() {
// 线程任务逻辑
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
两种方式都可以创建和执行多线程任务,选择使用哪种方式取决于具体的需求和场景:
- 如果任务逻辑比较简单,仅需重写
run()
方法,可以选择继承Thread
类的方式。 - 如果任务逻辑较为复杂,可能需要多个线程共享一个任务对象或将任务对象用作其他类的组件,或者需要实现多重继承等情况,可以选择实现
Runnable
接口的方式。
总体来说,使用Runnable
接口更加灵活,能够更好地遵循面向对象的设计原则,而继承Thread
类则更加简单直接。需要继承多个的时候就用接口。