进程与线程
- 进程:进程是操作系统资源分配的单位 例:I/O资源,内存资源...
- 线程:线程是资源调度的单位,真正执行的指令 例:操作数据的执行流
线程的创建
三种方法:继承Thread类、实现Runnable接口、实现Callable接口
线程的方法介绍
Thread
类常用操作线程方法:
start()
:启动线程,使线程进入可运行状态。run()
:包含线程要执行的具体逻辑代码。sleep(long millis)
:使当前线程暂停指定的毫秒数。yield()
:提示线程调度器当前线程愿意让出 CPU 资源,让其他具有相同优先级的线程有机会执行。join()
:等待调用该方法的线程结束。isAlive()
:判断线程是否处于活动状态。getPriority()
:获取线程的优先级。setPriority(int newPriority)
:设置线程的优先级。
如果有两个线程 Thread1
和 Thread2
, Thread1
想要等待 Thread2
执行结束后再继续执行,可以这样写:
Thread thread2 = new Thread(() -> {
// 线程 2 的执行逻辑
});
thread2.start();
thread2.join();
// 这里 Thread1 会等待 thread2 结束后再继续
设置线程的优先级:
Thread thread = new Thread(() -> {
// 线程执行逻辑
});
thread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
thread.start();
Volatile关键字
volatile
关键字在 Java 中是一个类型修饰符,用于提供线程之间的可见性和禁止指令重排序。
- 线程可见性:当一个变量被声明为
volatile
时,意味着对该变量的修改会立即被写入主内存,并且对其他线程是可见的。没有volatile
修饰时,线程可能会从本地缓存中读取变量值,导致不同线程看到的变量值不一致。 - 禁止指令重排序:编译器和处理器为了优化性能,可能会对代码的执行顺序进行重排序。但对于
volatile
变量的读写操作,不会与之前和之后的内存操作重排序。
在多线程环境下共享一个标志变量:
volatile boolean flag = false;
// 线程 1
public void setFlag() {
flag = true;
}
// 线程 2
public void checkFlag() {
if (flag) {
// 执行相应操作
}
}
如果 flag
没有使用 volatile
修饰,线程 2 可能无法及时看到线程 1 对 flag
的修改。需要注意的是,volatile
并不能保证原子性。例如,对于 volatile int count = 0;
,如果多个线程同时执行 count++
操作,仍然可能会出现数据不一致的问题。在这种情况下,需要使用同步机制(如锁)来保证操作的原子性。
Synchronized关键字
synchronized
关键字在 Java 中用于实现线程同步,确保在同一时刻只有一个线程可以执行被 synchronized
修饰的代码块或方法。
(1)修饰方法
- 当
synchronized
修饰一个实例方法时,锁定的是当前实例对象(this
)。 - 修饰一个静态方法时,锁定的是当前类的
Class
对象。
(2)修饰代码块
- 可以指定一个对象作为锁,常见的是
this
或者自定义的一个对象。
public class SynchronizedExample {
private int count = 0;
// 实例方法同步
public synchronized void increment() {
count++;
}
// 静态方法同步
public static synchronized void staticMethod() {
// 静态同步方法的逻辑
}
public void codeBlockSync() {
synchronized (this) {
// 同步代码块的逻辑
}
}
}
synchronized
主要作用:
-
保证线程安全:避免多个线程同时访问和修改共享数据时导致的数据不一致和错误。
-
解决竞态条件:防止多个线程竞争资源时出现不可预测的结果。
过度使用 synchronized
可能会导致性能下降,因为它会阻塞其他线程的执行。在实际应用中,需要根据具体的业务场景和性能要求,合理地使用 synchronized
或者选择其他更轻量级的同步机制(如 Lock
接口的实现类)。
线程安全其他机制:
-
final
关键字:当一个变量被声明为final
时,一旦被初始化,其值就不能被修改。对于引用类型的final
变量,不能重新指向新的对象,但对象的内容可以修改(如果对象本身不是不可变的)。 -
Atomic
类:如AtomicInteger
、AtomicLong
等,提供了原子性的操作,例如自增、自减等,无需使用同步机制就能保证线程安全。 -
Lock
接口及其实现类:如ReentrantLock
,比synchronized
更加灵活,可以实现更复杂的锁控制逻辑,例如尝试获取锁、限时获取锁等。 -
ThreadLocal:
为每个线程提供独立的变量副本,线程之间互不干扰,从而避免了多线程对共享变量的并发访问问题。 -
并发容器:如
ConcurrentHashMap
、ConcurrentLinkedQueue
等,这些容器在并发环境下进行操作时是线程安全的。
`final` 关键字修饰的变量可以被 `volatile` 修饰吗?
final
关键字修饰的变量可以被volatile
修饰,但这种情况相对较少见。通常情况下,如果一个变量是final
,其不可更改的特性已经能够满足大部分需求,不太需要再用volatile
修饰。final
修饰的变量一旦被初始化赋值,就不能再被修改其引用(对于引用类型)或值(对于基本数据类型)。volatile
主要用于保证变量的可见性和禁止指令重排序。- 当一个
final
变量同时被volatile
修饰时,它不仅具有不可更改性,还具有更强的线程可见性保证和禁止指令重排序的特性。
使用 AtomicInteger
实现线程安全的计数器:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterExample {
private AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
public int getCount() {
return counter.get();
}
public static void main(String[] args) {
AtomicCounterExample example = new AtomicCounterExample();
// 多个线程操作
}
}
使用 ReentrantLock
实现同步:
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
LockExample example = new LockExample();
// 多个线程操作
}
}