线程
使用线程方法
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
继承Thread vs 实现Runnable的区别
- 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档可以看到Thread类本身就实现了Runnable接口
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
线程常用方法
- setName:设置线程名称,使之与参数name相同
- getName:返回该线程的名称
- start:使该线程开始执行;Java虚拟机底层调用该线程的start0方法
- run:调用线程对象run方法
- setPriority:更改线程的优先级
- getPriority:获取线程的优先级
- sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- interrupt:中断线程(中断当前操作)
- yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
- join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
启动线程
- 继承Thread类:让重写run方法的类的对象调用start方法,即可启动另一个线程并自动调用重写的run方法
- 实现Runnable接口:创建Thread类对象,并把实现Runnable接口的类的对象作为Thread类构造参数,然后让Thread对象调用start方法,即可启动另一个线程并自动调用重写的run方法
把实现Runnable接口的类的对象作为Thread类构造参数:Thread底层使用了设计模式(代理模式)
start( ) 方法
start方法调用start0方法,start0方法是本地方法,是JVM调用,底层是C/C++实现
真正实现多线程效果的是start0方法,而不是run
start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态,具体什么时候执行,取决于CPU,由CPU统一调度。
用户线程和守护线程
用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
线程状态
https://blog.csdn.net/pange1991/article/details/53860651
- NEW:初始状态 尚未启动的线程处于此状态。
- RUNNABLE:运行时状态 在Java虚拟机中执行的线程处于此状态。
- Ready
- Running
- BLOCKED:阻塞状态 被阻塞等待监视器锁定的线程处于此状态
- WAITING:等待状态 正在等待另一个线程执行特定动作的线程处于此状态,
- TIMED_WAITING:超时等待 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED:终止状态 已退出的线程处于此状态
线程同步机制
同步和互斥是多线程编程中常用的两种机制,用于解决多个线程之间的协调和资源共享的问题。
- 同步(Synchronization): 同步是指控制多个线程之间的执行顺序,以避免彼此之间的冲突和混乱。在多线程环境下,当多个线程同时访问共享资源时,可能会导致数据不一致或错误的结果。通过同步机制,可以确保在某个线程访问共享资源时,其他线程无法同时修改该资源,从而保证数据的一致性。
- 互斥(Mutual Exclusion): 互斥是同步的一种具体实现,它通过一种锁的机制来保证在任意时刻只有一个线程可以访问共享资源,其他线程必须等待当前线程释放锁之后才能进入临界区。这样可以避免竞争条件和数据不一致的问题。
简单来说,同步是一种更广泛的概念,用于描述协调多个线程之间的操作顺序和数据访问方式,而互斥是同步的一种实现方式,通过锁机制来确保在同一时刻只有一个线程能够访问共享资源。
在实际的多线程编程中,同步和互斥通常是密切相关的,开发者需要根据具体的需求选择合适的同步和互斥机制,以确保多线程程序的正确性和稳定性。
synchronized 关键字(加互斥锁)
实现线程的同步,在多线程并发执行时保证线程安全。
- 同步方法:在方法声明中使用
synchronized
关键字,表示该方法是同步方法。当一个线程进入同步方法时,会自动获取该方法所属对象的锁(也称为内置锁或监视器锁),其他线程必须等待锁被释放后才能进入该方法。
public synchronized void synchronizedMethod() {
// 同步方法的代码块
}
静态方法的同步
public class abc Runnable {
public synchronized static void someMethod(){
//同步代码块
}
}
// 锁加在了abc.class上
- 同步代码块:在代码块中使用
synchronized
关键字,对指定的对象进行加锁。只有一个线程能够获得该对象的锁,其他线程必须等待锁被释放后才能执行该代码块。
public void someMethod() {
// 非同步代码块
synchronized (obj) {
// 同步代码块
}
// 非同步代码块
}
在静态方法的同步代码块中
public class abc Runnable {
public static void someMethod(){
// 非同步代码块
synchronized(abc.class){// 锁加在了abc.class上
//同步代码块
}
// 非同步代码块
}
}
注意事项和细节
- 同步方法如果没有使用static修饰:默认锁对象为this
- 如果方法使用static修饰,默认锁对象:当前类.class
使用 synchronized
可以有效避免多线程并发访问共享资源时出现的数据不一致或冲突的问题。通过对共享资源进行同步,确保每次只有一个线程能够修改该资源,其他线程需要等待。然而,过多地使用 synchronized
可能会导致性能问题,因此在设计多线程程序时需要慎重使用。
死锁
线程死锁是指两个或多个线程在互相等待对方释放资源的情况下,导致它们都无法继续执行的状态。当发生死锁时,每个线程都在等待另一个线程释放资源,从而导致它们之间形成僵局,无法继续执行下去。
典型的死锁情况通常涉及多个线程和多个共享资源,同时涉及到以下四个条件的出现:
- 互斥条件:资源只能被一个线程持有,不能同时被多个线程持有。
- 请求与保持条件:一个线程持有一个资源的同时,又请求另一个资源。
- 不剥夺条件:线程不能强行从另一个线程手中夺取资源,只能通过自愿释放来释放资源。
- 循环等待条件:一组线程形成循环等待其他线程持有的资源,即每个线程都在等待下一个线程所持有的资源。
避免死锁的方法包括但不限于以下几点:
- 避免使用多个锁:尽量减少使用多个锁,或者确保对多个锁的获取顺序是一致的,从而降低发生死锁的可能性。
- 避免持有锁的同时等待其他资源:尽可能减少持有一个锁的同时等待其他资源的情况,可以通过设计更合理的资源分配策略来避免这种情况。
- 使用超时机制:在获取锁时设定一个超时时间,超过该时间则放弃当前锁,释放资源,避免长时间等待而导致死锁。
- 使用专门的工具进行死锁检测:一些工具可以帮助检测和诊断死锁情况,及时发现并解决潜在的死锁问题。
在编写多线程程序时,需要谨慎设计线程之间的资源竞争关系,避免出现死锁情况,以确保程序的稳定性和可靠性。
锁的释放
锁的释放是通过退出 synchronized 块或方法来实现的。当线程执行完 synchronized 块或方法内的代码,或者通过调用 wait() 方法使线程进入等待状态时,锁会自动释放。
在使用 synchronized 关键字时,锁的获取和释放是由 Java 虚拟机来管理的,开发者无需手动释放锁。当一个线程获取到锁后,在退出 synchronized 块或方法时,锁会被自动释放,其他线程才有机会获取该锁继续执行。
除了自动释放锁外,Java 中还提供了 ReentrantLock 类来进行锁的控制,使用 ReentrantLock 类可以更灵活地控制锁的获取和释放。在使用 ReentrantLock 类时,需要手动调用 lock() 方法获取锁,调用 unlock() 方法释放锁,确保锁的正确释放。
以下是一个简单的示例代码,演示了如何使用 ReentrantLock 类手动释放锁:
public class LockExample {
private final Lock lock = new ReentrantLock();
public void performTask() {
lock.lock();
try {
// 执行需要同步的代码块
System.out.println("Performing task...");
} finally {
lock.unlock(); // 明确释放锁
}
}
public static void main(String[] args) {
LockExample example = new LockExample();
example.performTask();
}
}
在上述示例中,通过调用 ReentrantLock 对象的 lock() 方法获取锁,在执行完同步代码块后,通过调用 unlock() 方法明确释放锁,确保资源得到正确释放。这种手动释放锁的方式可以帮助避免死锁等问题,但也需要开发者谨慎处理锁的获取和释放逻辑。
下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用Thread.sheep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起该线程不会释放锁。
注意:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用
线程的等待和唤醒
https://zhuanlan.zhihu.com/p/453278516
- wait()方法
首先我们需要了解wait()方法的继承体系,他是在Object对象的基类方法,也就是说所有的对象都拥有wait()方法。一个线程调用了java的wait()方法后,当前线程会被阻塞挂起,这里的调用指的是线程里面调用了加锁对象的wait()方法。
线程被阻塞挂起后是需要唤醒的,下面会讲到唤醒方法,但是也可以调用重载方法wait(long timeout),让线程被阻塞后超过一定时间还没被唤醒而自动唤醒。
- notify()方法
notify()方法也是继承于Object对象。当某个线程调用了加锁对象的notify方法后,会唤醒之前在该对象进行获取监视器锁时失败而被阻塞的线程,如果有多个线程同时被阻塞,notify()方法只会有一个线程被唤醒,如果需要唤醒全部,则可以调用notifyAll()方法。
所以面试中会被问到wait和notify的作用,可以侧重的知识点是:
- 调用wait之前一定是获取到锁的,所以要保证在synchronized块中。
- 调用wait后会释放该对象的锁。
- 调用notify()方法也要是获取锁, 也要保证在synchronized块中。
- 调用notify()方法唤醒一个线程,调用notifyAll()方法唤醒全部被阻塞线程。
- 调用notify()或者notifyAll()方法只是唤醒了其他被阻塞的线程,他们有了重新竞争锁的条件,但是当前线程还没有释放锁的,只有调用了wait()方法才会释放锁。
用一个生产者消费者模型来看看wait和notify的用法。生产者消费者模型可以简单理解为有一个容器,当里面没有数据时生产者会往里面添加数据,满了则暂停当前的工作等待消费者消费数据后通知他继续添加。消费者会往里面拿数据,没有了数据则暂停工作等待生产者生产了数据并通知他继续消费。
public static void main(String[] args) {
Object lock = new Object();
AtomicInteger counter = new AtomicInteger(0);
Queue<Integer> queue = new LinkedList<>();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (true) {
//如果队列没有数据,调用wait()方法,阻塞自己
if (queue.isEmpty()) {
try {
System.out.println("消费者线程阻塞");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果队列不为空,消费数据;如果线程被生产者通过notifyAll()方法唤醒后,线程重新获取到锁时是从这里执行的
System.out.println("消费者线程消费数据: " + queue.poll());
//消费者消费后,唤醒可能由于之前队列满了而主动阻塞自己的生产者
lock.notifyAll();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (true) {
//如果队列数据满了,调用wait()方法,阻塞自己
if (queue.size() > 10) {
System.out.println("生产者线程阻塞");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果队列没有满,生产数据; 如果被其他线程唤醒,在下次获取到锁的时候生产数据
System.out.println("生产者线程生产数据");
queue.add(counter.incrementAndGet());
//队列有数据了,唤醒之前可能没有数据而主动祖寺啊自己的消费者
lock.notifyAll();
}
}
}
}).start();
}