为什么要学习多线程?
Java多线程与并发是当前面试的热点,你如果想要找到工作或者高薪水,是必须要熟练掌握的。如Boss直聘上关于Java专家与初级开发工程师的招聘要求:
回忆这几年编码工作经历,我整理了关于多线程方面的知识,由浅入深帮助你了解多线程方面的知识。
什么是进程
进程是操作系统资源分配的最小单位,如下任务管理器中的都是进程,PID是进程id。通俗点就是win10、Mac、linux这些电脑操作系统都是以进程为单位进行资源分配(磁盘空间,内存空间等)。
什么是线程
操作系统调度执行的最小单位,进程得到系统分配的资源后,需要调用执行任务,每个任务都是以线程的方式去执行的。如上图中的任务管理器,PID14436,这个进程有3个线程在执行。
线程的状态
创建线程的方法
1.实现Runnable方法
public class MyThread implements Runnable {
@Override
public void run() {
// 在新的线程中执行的任务
}
}
2.实现Thread类
public class MyThread extends Thread {
@Override
public void run() {
// 在新的线程中执行的任务
}
}
线程优先级
线程优先级是操作系统用来调度线程的一种机制。在操作系统中,每个线程都有一个优先级,表示该线程需要执行的顺序。当多个线程同时竞争CPU时间片时,操作系统会根据线程的优先级来决定哪个线程先执行。
线程优先级的取值范围通常为1到10,其中1表示最低优先级,10表示最高优先级。
线程控制
Thread Joining:当一个线程执行完毕后,它会调用Thread.join()方法等待其他线程的执行。在等待期间,该线程将处于阻塞状态,直到所有其他线程都执行完成。
Thread Sleep:Thread.sleep()方法可以让线程暂停一段时间,以便让其他线程有机会执行。通过使用Thread.sleep()方法,我们可以控制线程的执行时间和优先级。
Thread Yield:Thread.yield()方法可以让当前线程放弃CPU控制权,以便让其他更高优先级的线程有机会执行。通过使用Thread.yield()方法,我们可以避免线程之间的竞争和死锁问题。
Thread Interrupts:当一个线程被中断时,它会抛出InterruptedException异常。在多线程编程中,我们可以使用Thread.interrupt()方法来中断正在运行的线程,并通过catch语句来处理中断异常。
线程上下文切换
由于电脑的资源是有限的,操作系统对CPU进行时间分片,每个线程都会得到自己的cpu执行时间,线程A比如得到10毫秒的CPU执行时间,10毫秒过完,不管线程A是否执行完成,都会轮到线程B执行。线程B的10毫秒执行完成才有可能轮到线程A继续执行。这个过程就叫做线程上下文切换。
为什么会有线程安全问题
多线程环境下,可以同时访问某个共享资源会出现数据不一致、死锁等问题,就会出现线程安全问题。举个例子,你银行卡上有5000块,这时你爷爷与奶奶同时你转1000块红包,如果转账过程多线程执行,都是读取的5000块,然后执行+1000操作,导致最后你到账6000,而不是7000,这就是线程安全问题。
锁机制
线程的锁机制是一种同步机制,用于确保多个线程不会同时访问共享资源,以避免出现竞态条件(上面的红包转账问题就是竞态条件)和死锁等问题。常见的锁包括互斥锁(Mutex)、读写锁(Read-Write Lock)和自旋锁(Spin Lock):
互斥锁:又叫排它锁,它允许一个线程持有锁并阻止其他线程访问共享资源,直到该线程释放锁为止。当一个线程获取了锁时,其他线程必须等待该线程释放锁后才能再次尝试获取锁。
读写锁:允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这样可以提高读取操作的性能,避免写操作阻塞所有读操作。
自旋锁:通过循环来不断地尝试获取锁。当一个线程获取了锁时,如果其他线程也试图获取锁,则该线程会一直循环等待,直到获取到锁为止。
线程饥饿
线程饥饿分为以下两种情况:
1.假设有多个线程正在等待共享资源。其中一个线程获取了资源但在完成任务后未能释放它,导致其他线程无限期地阻塞。因此,这些线程无法执行它们的任务,饿死了。
2.当一个较高优先级的线程不断获取资源时,较低优先级的线程无法获得资源,导致较低优先级的线程饿死。
线程饥饿可能导致吞吐量降低、性能差以及程序完全挂起等问题。因此,有效地管理资源并确保及时释放资源以避免线程饥饿是非常重要的。
死锁问题
假设有两个线程A和B,它们需要访问共享资源R。每个线程都需要持有两个锁才能访问R:
线程A需要先获得锁1,然后再获得锁2才能访问R。
线程B需要先获得锁2,然后再获得锁1才能访问R。
现在假设这两个线程同时运行,它们会按照以下顺序获取锁:
线程A获取锁1,然后它尝试获取锁2。
线程B获取锁2它继续尝试获取锁1。
两个线程由于需要对方的锁,从而进入相互等待,这就是死锁。
如何检测死锁:
可以使用Java自带的jconsole、jvisualvm等监控工具,或者jstack、jmap等命令行工具
如下代码,定义了两个线程A与B,两者互相等待引起死锁
public static void main(String[] args) throws Exception {
Object lock1=new Object();
Object lock2=new Object();
Thread threadA=new Thread(()->{
synchronized (lock1){
try {
System.out.println("线程A获取锁1");
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2){
System.out.println("线程A获取锁2");
}
}
},"线程A");
Thread threadB=new Thread(()->{
synchronized (lock2){
try {
System.out.println("线程B获取锁2");
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock1){
System.out.println("线程B获取锁1");
}
}
},"线程B");
threadA.start();
threadB.start();
}
Java visualVM