Java多线程详解(很详细)
Java多线程详解
文章目录
- Java多线程详解
- 一、前言
- 二、进程与线程
- 三、Java 实现多线程的四种方式
- 3.1 继承Thread类,重写run方法
- 3.2 实现Runnable接口创建线程
- 3.3 实现Callable接口创建线程
- 3.4 通过线程池创建线程
- 四、线程的运行机制
- 五、线程的五种状态
- 5.1 新建状态(New)
- 5.2 就绪状态(Runnable)
- 5.3 运行状态(Running)
- 5.4 阻塞状态(Blocked)
- 5.5 死亡状态(Dead)
- 六、线程的调度以及优先级
- 6.1.线程的调度
- 6.2 线程的优先级
- 6.3 线程睡眠和线程让步、线程同步
- 七、LOCK锁和Synchronized同步锁
- 7.1 Lock锁
- 7.1.1 ReentrantLock(可重入锁)
- 7.1.2 ReadWriteLock(读写锁)
- 7.2 Lock锁和synchronized同步锁的区别
- 八、总结
一、前言
在Java的开发中,处处都会用到多线程,现在开发大多都会使用到Spring的框架,里面就封装了很多多线程相关的代码,只是我们在平时的开发中感受不到,比如tomcat 的连接池就使用到了线程池技术,所以了解Java多线程是学习Java至关重要的一步!
二、进程与线程
- 进程是指运行中的应用程序,每一个进程都有自己独立的内存空间
- 线程是指进程中的一个执行流程,线程也是CPU调度的最小单位,有时候也成为执行情景
- 一个进程可以由多个线程组成,即一个进程中可以同时运行多个线程,他们分别执行不同的任务,一个进程至少包含一个线程
- 当进程内的多个线程同时运行时,这种运行方式称为并发运行
三、Java 实现多线程的四种方式
3.1 继承Thread类,重写run方法
- 首先继承Thread类,然后重写run()方法,run()方法里面写线程的方法体
- 在需要启动线程的时候调用start()方法开启线程
注意: 不要使用run()方法开启线程
public class ThreadService extends Thread{public void run(){for (int i = 0; i < 50; i++) {System.out.println("我是副线程:"+i);}}public static void main(String[] args) {ThreadService threadService=new ThreadService();threadService.start();for (int i = 0; i < 50; i++) {System.out.println("我是主线程:"+i);}}
}
//运行结果如下
我是主线程:12
我是主线程:13
我是主线程:14
我是副线程:0
我是主线程:15
我是主线程:16
我是副线程:1
我是副线程:2
...........
3.2 实现Runnable接口创建线程
- 首先实现Runnable接口
- 在main方法中创建一个实现该接口的对象并传进Thread类的匿名对象内
- 使用Thread类的匿名对象开启线程
优点: 可避免单继承的局限性,方便一个对象被多个线程使用
package com.study.threadStudy;
public class MyRunnableThread implements Runnable{@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println("实现Runnable接口创建的副线程:"+i);}}public static void main(String[] args) {//创建实现Runnable接口的类的对象MyRunnableThread myThread=new MyRunnableThread();//使用Thread类的匿名对象调用start()方法new Thread(myThread).start();for (int i = 0; i < 50; i++) {System.out.println("main方法内部的主线程:"+i);}}
}//运行结果
main方法内部的主线程:5
main方法内部的主线程:6
实现Runnable接口创建的副线程:0
main方法内部的主线程:7
实现Runnable接口创建的副线程:1
main方法内部的主线程:8
实现Runnable接口创建的副线程:2
main方法内部的主线程:9
实现Runnable接口创建的副线程:3
...........
3.3 实现Callable接口创建线程
- 实现Callable接口(需要定义返回值类型),重写call方法(需要抛出异常)
- 创建实现Callable接口实现类的对象
- 将实现类对象作为参数传递给FutureTask构造参数
- 将FutureTask对象作为参数传递给Thread构造函数(因为FutureTask实现了Runnable接口,所以可以这么传)
- 调用Thread类的start()方法启动线程
这种创建线程的方法一般用于需要很长时间的操作但是我们只需要结果的情况
比如我们需要进行一个算法运算,这个运算的过程非常繁琐,需要很长的时间才能完成,但是我们最终只需要它的运行结果而已
那么我们就可以先在要获取结果前多一段时间就运行该线程,在最终需要结果的地方去获取结果
但是注意,如果执行了获取结果的方法但是那个线程还没有得到结果的话,线程会阻塞直到执行结果出来
package com.study.threadStudy;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class MyCallableThread implements Callable {@Overridepublic Object call() throws Exception {int sum = 0;for (int i = 0; i < 100; i++) {if (i % 2 == 0) {sum += i;}}return sum;}public static void main(String[] args) throws ExecutionException, InterruptedException {//创建实现Callable接口的对象MyCallableThread myThread = new MyCallableThread();//将此Callable接口的实现类作为参数传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(myThread);//将FutureTask的对象作为参数传递到Thread的构造器中,创建Thread对象并调用start方法new Thread(futureTask).start();//这里可以继续执行主线程,需要结果的时候再来找子线程获取结果for (int i = 0; i < 5; i++) {System.out.println("我是主线程:" + i);}//获取子线程的结果,这里需要注意的是如果子线程计算还没有完毕,这里调用get方法就会造成阻塞哦Object sum = futureTask.get();System.out.println("我是子线程的结果:" + sum);}
}//执行结果
我是主线程:0
我是主线程:1
我是主线程:2
我是主线程:3
我是主线程:4
我是子线程的结果:2450
Callable和Runnable的区别: Callable接口中定义了需要有返回的任务需要实现的call方法,比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果
3.4 通过线程池创建线程
-
使用线程池的原因
通过Thread创建线程在执行完就被销毁了, 不可服用. 在高并发场景中, 频繁创建线程是非常消耗资源的, 通过线程池创建线程可以对已经创建好的线程进行复用
-
线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
-
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- Executors.newWorkStealingPool(int):jdk8新增,使用目前机器上可用的处理器作为它的运行级别。
-
创建线程池的步骤
- 调用Executors的newFixedThreadPool(),返回指定线程数量的ExecutorService
- 将Runnable接口的实现类对象传递给ExecutorService的Executor()方法中,开启线程并执行
- 关闭线程池
package com.study.threadStudy;import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class MyThreadPoll {private static int POOL_NUM=10;//定义线程池的数量public static void main(String[] args) {//调用Executors的newFixedThreadPool(),返回指定线程数量的ExecutorServiceExecutorService executorService= Executors.newFixedThreadPool(10);//将Runnable接口的实现类对象传递给ExecutorService的Executor()方法中,开启线程并执行executorService.execute(new RunnableThread());executorService.execute(new RunnableThread());executorService.execute(new RunnableThread());//关闭线程池executorService.shutdownNow();}public static class RunnableThread implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() +"---"+ i);}}} } //执行结果 通过线程池方式创建的线程:pool-1-thread-1---0 通过线程池方式创建的线程:pool-1-thread-3---0 通过线程池方式创建的线程:pool-1-thread-2---0 通过线程池方式创建的线程:pool-1-thread-2---1 通过线程池方式创建的线程:pool-1-thread-3---1 通过线程池方式创建的线程:pool-1-thread-1---1 通过线程池方式创建的线程:pool-1-thread-3---2 通过线程池方式创建的线程:pool-1-thread-2---2 通过线程池方式创建的线程:pool-1-thread-3---3 通过线程池方式创建的线程:pool-1-thread-3---4 通过线程池方式创建的线程:pool-1-thread-1---2 通过线程池方式创建的线程:pool-1-thread-3---5 通过线程池方式创建的线程:pool-1-thread-2---3 通过线程池方式创建的线程:pool-1-thread-3---6 通过线程池方式创建的线程:pool-1-thread-3---7 通过线程池方式创建的线程:pool-1-thread-3---8 通过线程池方式创建的线程:pool-1-thread-3---9 通过线程池方式创建的线程:pool-1-thread-3---10 通过线程池方式创建的线程:pool-1-thread-3---11 通过线程池方式创建的线程:pool-1-thread-3---12 ..........................................
四、线程的运行机制
每个线程都有一个独立的程序计数器和方法调用栈,扩展:JVM中程序计数器和方法栈是每个线程独有的,而堆和方法区是所有线程共享的
-
程序计数器:也称之为PC寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令
-
方法调用栈:简称方法栈,用来跟踪线程运行中一系列方法的调用过程,栈中的元素简称为栈帧,每当线程调用一个方法,就会向方法栈中压入一个新帧,桢用来存储方法的参数、局部变量和运算过程中的临时数据。
-
栈区先是为空,当调用main方法时,便将main方法压入栈,称为栈帧,后调用method方法,就将method方法加入栈,成为栈帧;
-
线程运行中所需要的:

五、线程的五种状态

5.1 新建状态(New)
用new语句创建的线程对象处于新建状态,此时他和其它的java 对象一样仅在堆区中被分配了内存
5.2 就绪状态(Runnable)
当一个线程对象被创建后,其它线程调用它的start()方法,该线程就进入了就绪状态,Java虚拟机会为它创建方法栈和程序计数器,处于这个状态的线程位于可运行池中,等待CPU的使用权
5.3 运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码,在并发环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态,如果计算机有多个CPU,那么同一时刻可以让多个线程占用不同的CPU,使他们都处于运行状态
5.4 阻塞状态(Blocked)
线程因为某些原因放弃CPU的使用权,暂时停止运行,此时进入阻塞状态,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态!
根据阻塞原因不同,阻塞状态又分为三种:
- **等待阻塞:**当前线程处于运行状态,如果执行了某个方法的wait()方法,Java虚拟机就会把这个线程放在对象等待池中。
- **同步阻塞:**当线程处于运行状态,视图获取某个对象的同步锁(Synchronized)时,如果该对象的同步锁(synchronized)已经被其它线程占用,Java虚拟机就会把这个线程放入到对象的锁池中。
- **其它阻塞:**当前线程调用了sleep()方法或者调用了其它线程的join()方法,或者发出了I/O请求,就会进入这个状态,当sleep()方法超时,join()方法等待线程终止或者超时、或者I/O处理完毕时,当前线程将重新转为就绪状态。
简述:遇到以下几种情况,线程就会从运行状态进入到阻塞状态
- 调用sleep()方法,使线程睡眠
- 调用wait()方法,使线程进入等待
- 当线程去获取同步锁的时候,锁正在被其它线程所持有
- 调用阻塞式IO方法会导致线程阻塞
- 调用suspend方法,挂起线程,也会造成阻塞。
需要注意的是:阻塞状态只能进入就绪状态,不能直接进入运行状态,因为从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定,只有当前线程获得了CPU的时间片之后,才会进入运行状态。
5.5 死亡状态(Dead)
当线程退出run()方法,就进入了死亡状态,该线程结束生命周期,线程有可能是正常执行完run()方法而退出,也有可能是遇到异常而退出,另外直接调用stop()方法也会停止线程,但是此方法已经被弃用,所以不推荐使用

六、线程的调度以及优先级
6.1.线程的调度
-
Java虚拟机采用抢占式调度模型,它是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中线程的优先级相同,那么就随机选择一个线程,使其占用CPU,处于运行状态的CPU会一直运行,直至他不得不放弃CPU,一个线程会因为以下原因而放弃CPU
1).Java虚拟机让当前线程暂时放弃CPU转为就绪状态,使其它线程获得运行机会
2).当前线程因为某种原因而进入阻塞状态
3).线程运行结束
-
如果明确想要一个线程给另一个线程运行的机会,可以采取以下办法之一
1).调整各个线程的优先级
2).让处于运行状态的线程调用Thread.sleep()方法
3).让处于运行状态的线程调用Thread.yield()方法(yield()做的是暂停当前正在执行的线程,使其放弃拥有的CPU资源,让当前运行线程回到可运行状态,以允许相同优先级的线程获得运行机会)
4).让处于运行状态的线程调用另一个线程的join()方法(将当前线程挂起,等待被调用线程执行完毕后,继续执行当前线程,这里需要注意的是,会阻塞当前线程哦)
6.2 线程的优先级
Thread类的setPriority(int)和getPriority(int)方法分别是用来 设置优先级和读取优先级的,优先级用整数表示,取值范围是 1~10,Thread类有三个静态常量:
- MAX_PRIORITY: 取值为10,表示最高优先级
- MIN_PRIORITY:取值为1,表示最低优先级
- NORM_PRIORITY:取值为5,表示默认的优先级
//模拟给线程设置优先级
Thread thread=new Thread();
thread.setPriority(MAX_PRIORITY);
6.3 线程睡眠和线程让步、线程同步
- 线程睡眠:当一个正在运行中的线程执行了Thread类的sleep()方法,他就会放弃CPU转到阻塞状态
- 线程让步:当一个正在运行中的线程执行了Thread类的**yield()**方法,则Java虚拟机会暂停当前正在执行的线程,使其放弃拥有的CPU资源,使具有相同优先级或者更高优先级的其它线程处于就绪状态。如果没有相同或更高优先级的可运行线程,yield()方法什么都不做。
- 当前运行的线程可以调用另一个线程的join()方法,当前运行的线程将转到阻塞状态,直至另一个线程运行结束,它才会恢复运行
- 线程同步:为了保证每个线程都能正常执行原子操作,Java引入了同步机制,具体的做法是在代表原子性操作的代码前加入 synchronized关键字,这样的代码被称为同步代码块
七、LOCK锁和Synchronized同步锁
7.1 Lock锁
- lock锁并不是Java内置的功能,其应用场景是在多线程并发访问时,为了避免冲突,需要每个线程先获取锁,避免其它线程的进入,等线程执行完后释放锁,允许其它线程进入。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该首先获得Lock对象
7.1.1 ReentrantLock(可重入锁)
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的的控制中,比较常用的是ReentrantLock,可以显示的加锁和释放锁,ReentrantLock是一种排它锁,同一时刻只允许一个线程访问
7.1.2 ReadWriteLock(读写锁)
ReentrantReadWriteLock读写锁提供了两个方法,readLock()和writeLock()用来获取读锁和写锁,也就是说将文件的的读写操作分开,分成两个锁来分配给线程,使得多个线程可以同步操作
- **读锁:**也称为共享锁,是线程共享的,用于读取操作的同步锁,锁对象是ReentrantReadWriteLock.readLock
- **写锁:**也称为独占锁,线程独占的,用于写入操作的同步锁,锁对象是ReentrantReadWrite.writeLock
- 读锁可以共享,写锁只有在所有读锁释放后才能执行,但是当写锁在阻塞和获取过程中,之后的读锁也会阻塞,需要等到写锁释放后才能获取
使用实例:
package com.study.threadStudy;
import java.util.concurrent.locks.ReentrantLock;
public class MyLockThread implements Runnable{//定义Lock锁对象private final ReentrantLock lock=new ReentrantLock();@Overridepublic void run() {//加锁lock.lock();try {//保证线程安全的代码System.out.println("这里是子线程的方法体");} finally {//释放锁lock.unlock();//如果同步代码有异常,需将unlock()写入finally语句块}}public static void main(String[] args) {//创建Runnable接口对象的实现类MyLockThread thread=new MyLockThread();//通过Thread的构造方法创建线程并调用start()方法开启线程new Thread(thread).start();System.out.println("这里是主线程的方法体");}
}
7.2 Lock锁和synchronized同步锁的区别
- Lock是一个接口,而synchronized是Java的关键字,synchronized是内置的语言实现的
- Lock是显示锁(需要手动开启和关闭锁,不要忘记关锁),synchronized是隐式锁,处理完作用域自动释放
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会造成死锁,而Lock锁在发生异常时,如果没有主动通过unlock()方法去释放锁,则会造成死锁现象,所以使用Lock时需要在finally块中释放锁。
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有很好的扩展性
- synchronized无法判断锁的状态,而Lock可以知道线程有没有拿到锁
- 优先使用顺序:lock>同步代码块
八、总结
Java多线程在开发中是至关重要的,所以弄懂多线程是很有必要的,以上只是自己的学习记录,如有不足欢迎指出,后续也会不断更新和优化内容
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
