博客中相关代码的git 地址:
多线程学习(一)基础知识 | 地址:https://blog.csdn.net/qq_40119805/article/details/107399842 |
---|---|
多线程学习(二)线程安全 | 地址:https://blog.csdn.net/qq_40119805/article/details/107432575 |
多线程学习(三)线程通信 | 地址:https://blog.csdn.net/qq_40119805/article/details/107456166 |
本章主要介绍:
1.什么是线程 2.如何创建线程 3.如何启动线程 4.线程如何停止 5.线程如何暂停 6.线程的常用方法
1.进程和多线程的概念及线程的优缺点
1.1什么是进程,什么是线程,两者区别
首先两者都是操作系统的 “单位”,进程是操作系统的基本单位,线程是操作系统的最小调度单位;进程包含线程;
进程:
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程:
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
1.2线程带来的好处
多线程能够充分利用系统资源,同时也会带来线程安全问题;
好处:
程序执行需要使用到多种操作系统的多种资源,比如cpu,磁盘,内存,单线程情况下当程序在等待磁盘数据读取或者写入时cpu会进入闲置状态,造成资源浪费,而多线程可以让程序在cpu上交替执行,充分利用cpu/操作系统资源;
单线程造成资源浪费:
多线程充分利用系统资源:
1.3线程带来的问题
非线程安全是指多个线程对同一个实例变量进行修改可能会出现值不同步的问题;
问题:更改一个变量需要三个步骤 获取值–>修改–>存储,多线程下可能会出现如下情况:
有变量count=0;A 线程获取值count(这时为0) ,对其进行+1 操作,还没来得及存储,B线程也执行了同样的操作,获取count值 (读到的数据为0) 并+1,然后A线程进行存储 修改变量count为 1,B线程随后也进行存储 修改变量count为 1。不符合预期 count = 2;这就是并发安全问题;
例子:两条线程同时执行 +1 操作各1W次,理论结果 i 应该等于 2W
package com.company.并发安全示例;
public class Main{
public static int i = 0;
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
MyThread myThread1 = main.new MyThread();
MyThread myThread2 = main.new MyThread();
myThread1.start();
myThread2.start();
Thread.sleep(10000);
System.out.println("线程执行了:"+i);
}
class MyThread extends Thread{
public void run(){
for (int j = 0; j <= 10000; j++) {
i++;
}
}
}
}
控制台显示:线程执行了 1.2W次 不符合2W执行的预期;
2.多线程的创建和启动
线程创建有四种方式,一个类 Thread,两个接口 Runnable callable,还有线程池
2.1使用Thread 创建线程
package 线程创建的四种方式;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
}
}
2.2使用Runnable创建线程
package 线程创建的四种方式;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
2.3使用Callable创建线程
package 线程创建的四种方式;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
2.4线程的启动
package 线程创建的四种方式;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
public class TheadTest {
public static void main(String[] args) {
// 继承Thread
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
// 实现Runable
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
// 实现Callable
MyCallable callable = new MyCallable();
FutureTask<Integer> integerFutureTask = new FutureTask<>(callable);
new Thread(integerFutureTask).start();
}
}
3.线程的暂停和停止
3.1停止线程
停止线程在java语言中并不像break语句那样干脆,需要一些技巧性处理,一旦处理不好就会导致出现一些超预期的行为和难以定位的错误;
首先停止线程有3种方法:
1.使用退出标志,使线程正常退出;
2.使用stop方法;(不推荐使用)
3.使用interrupt方法中断线程;
3.2 退出标志停止线程
退出标志是最简单的线程停止方法,但是退出逻辑需要自己写;
package 线程的停止.退出标志停止;
public class MyThread extends Thread {
public static boolean mark = false;
@Override
public void run() {
while (true){
if (mark) {
System.out.println("线程停止");
break;
}
System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
}
}
}
package 线程的停止.退出标志停止;
public class Main {
public static void main(String[] args) throws InterruptedException {
new MyThread().start();
Thread.sleep(100);
MyThread.mark = true;
}
}
控制台:线程正确停止
3.2 stop方法暴力停止线程
stop是暴力停止线程,该方法已经被弃用,因为其可能会导致数据不一致;
正常运行示例:
package 线程的停止.stop方法停止线程;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
Thread.sleep(100);
myThread.stop();
}
class MyThread extends Thread {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
}
}
}
}
运行截图:
stop方法可能导致数据不一致,例如 有 user 类,两个字段 name= “a”,password = “aa”,使用方法setNameAndPassword()对执行 name = “b” ,password = “bb”,但是name = “执行完成”,password=“bb”还没来得及执行,线程外部就使用stop方法停止,这样就会出现 name = “b”,password=“aa”的情况;
package 线程的停止.stop方法停止线程;
public class Main {
static User user = new User();
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
Thread.sleep(100);
myThread.stop();
System.out.println(user.toString());
}
class MyThread extends Thread {
@Override
public void run() {
user.setNameAndPassword("b","bb");
}
}
static class User {
private String name = "a";
private String password = "aa";
public void setNameAndPassword(String name, String password) {
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
}
运行结果图:
3.3 interrupt()中断方法停止线程;
interrupt和退出标志基本一样,只不过interrupt更加规范,不用自己定义标志;并且有对应方法来检测中断状态,和清除中断状态;
interrupt()一般配合return 或者 异常来终止线程。异常法要优于return,因为在catch块中还可以向上抛出异常,使得线程停止的事件得以传播;
使用方法:线程外部使用 thread.interrupt()来标记线程停止;线程内部 使用 this.isInterrupted() 来获取标志的值;
1.代码示例:interrupt()+ return 停止线程
package 线程的停止.interrupt方法停止线程;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
myThread.interrupt();
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
if (this.isInterrupted()){
return;
}
System.out.println(i);
}
}
}
}
2.代码示例:interrupt()+ 异常法停止线程
package 线程的停止.interrupt方法加异常停止线程;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
Thread.sleep(100);
myThread.interrupt();
}
class MyThread extends Thread {
@Override
public void run() {
int i = 0;
try {
while (true) {
System.out.println(i++);
if (this.isInterrupted()) {
throw new InterruptedException();
}
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.4 检测线程中断状态
使用interrupt()方法中断线程后,可以使用 interrupted()方法检测中断状态,再次使用该方法可以清除中断状态
this.isInterrupted()也拥有 interrupted()方法同样的功能,但是第二次调用不会清除中断状态;
示例代码:
package 线程的停止.检测线程状态;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
myThread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while (true){
if (this.isInterrupted()){
System.out.println("检测中断:"+Thread.interrupted());
System.out.println("再次调用清除中断状态:"+Thread.interrupted());
return;
}
System.out.println(i++);
}
}
}
}
运行结果:
3.5 线程的暂停
线程可使用suspend 暂停 resume恢复 运行
代码示例:示例中打印输出 计数i 和系统当前时间,控制台结果可见1秒内仅执行i++2048次,证明suspend正确运行;
package 线程的暂停.suspend和resume;
import 线程的停止.退出标志停止.MyThread;
public class Main {
public static int i =0;
public static void main(String[] args) throws InterruptedException {
Mythread mythread = new Main().new Mythread();
mythread.start();
System.out.println(i+"-"+System.currentTimeMillis());
mythread.suspend();
Thread.sleep(1000);
mythread.resume();
System.out.println(i+"-"+System.currentTimeMillis());
}
class Mythread extends Thread{
@Override
public void run() {
for (i = 0; i <100000 ; i++) {
// System.out.println(i);
}
}
}
}
控制台结果:
3.6线程暂停带来的问题–独占
使用supend方法暂停线程时如果没有进行resume,则会导致锁无法释放;暂停的线程会一直持有锁;
涉及到线程同步,独占问题将会在下一章说明
4.线程的常用方法
线程常用方法:
getPriority():获取当前线程的优先级
setPriority():设置当前线程的优先级
interrupt():中断线程
Thread.currentThead():获取当前线程对象
isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)
以下方法涉及到锁的问题,会在之后章节进行讨论:
join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。
sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。
yield():调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)
wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的
notify():唤醒当前线程,进入运行状态。该方法要在同步方法或者同步代码块中才使用的
notifyAll():唤醒所有等待的线程。该方法要在同步方法或者同步代码块中才使用的
代码示例:
package 线程的常用方法;
public class Main {
public static int i =0;
public static void main(String[] args) throws InterruptedException {
Mythread mythread = new Main().new Mythread();
mythread.start();
//isAlive() 方法判断线程是否存活
System.out.println(mythread.isAlive());
//sleep() 方法使线程进入睡眠状态
Thread.sleep(1);
//getId 获取线程唯一标识
System.out.println(mythread.getId());
//setPriority设置线程优先级 getPriority获取线程优先级
mythread.setPriority(1);
System.out.println(mythread.getPriority());
}
class Mythread extends Thread{
@Override
public void run() {
//currentThread() 方法可以获取当前线程的基本信息 线程名称 线程id 线程优先级等
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getId());
//yield 放弃cpu资源
Thread.yield();
for (i = 0; i <100000 ; i++) {
// System.out.println(i);
}
}
}
}
5.线程的状态和状态转换
5.1 操作系统中的线程状态
在操作系统中线程状态分为五种:
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,当调用线程对象的
start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才
有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting
queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized
同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态; (三). 其他阻塞:
通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
5.2 java中的线程状态
5.2.1 线程状态和他们之间的关系
首先看下jdk1.8官方文档的说明:
再看下线程状态关系:
5.2.2 验证 new ,runnable 和 terminated
线程创建后调用start()方法前时new状态,调用后时可运行状态,线程结束后时terminated状态;
值得注意的是 在线程的构造方法中打印的线程状态其实是main方法的状态;
代码示例:
package 线程关系的验证.new_runable_terminated;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
System.out.println("状态一:"+myThread.getState());
Thread.sleep(100);
myThread.start();
Thread.sleep(100);
System.out.println("状态一:"+myThread.getState());
}
class MyThread extends Thread{
MyThread(){
System.out.println("线程构造时的状态:"+Thread.currentThread().getState());
}
@Override
public void run() {
System.out.println("线程运行时的状态:"+Thread.currentThread().getState());
}
}
}
控制台:
5.2.3 Timed Waiting 状态验证
sleep()都会使线程进入Timed Waiting状态
package 线程关系的验证.TimedWaiting;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new Main().new MyThread();
myThread.start();
Thread.sleep(100);
System.out.println("状态:"+myThread.getState());
}
class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
5.2.4 Blocked 状态验证
线程等待锁的时候会出现Blocked 状态
package 线程关系的验证.Blocked;
public class Main {
public static String temp = "1";
public static void main(String[] args) throws InterruptedException {
MyThread myThread1 = new Main().new MyThread();
MyThread myThread2 = new Main().new MyThread();
myThread1.start();
Thread.sleep(100);
myThread2.start();
Thread.sleep(100);
System.out.println("状态线程2的状态:"+myThread2.getState());
}
class MyThread extends Thread{
@Override
public void run() {
synchronized(temp){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
5.2.4 Waiting 状态验证
使用wait()方法后会进入WAITING状态
package 线程关系的验证.Waiting;
public class Main {
public static String temp = "1";
public static void main(String[] args) throws InterruptedException {
MyThread myThread2 = new Main().new MyThread();
myThread2.start();
Thread.sleep(100);
System.out.println("状态线程2的状态:"+myThread2.getState());
}
class MyThread extends Thread{
@Override
public void run() {
synchronized(temp){
try {
temp.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
6.线程池
池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
示例代码:
package 线程创建的四种方式;
import java.util.concurrent.*;
public class TheadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
MyRunnable runnable = new MyRunnable();
MyCallable callable = new MyCallable();
// 四种线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
ExecutorService executorService1 = Executors.newFixedThreadPool(1);
ExecutorService executorService2 = Executors.newCachedThreadPool();
ExecutorService executorService3 = Executors.newScheduledThreadPool(100);
//使用线程池启动 线程
for (int i = 0; i <2 ; i++) {
executorService.execute(myThread);
executorService1.execute(runnable);
//executorService2.execute(callable); callable需要使用
Future<Integer> submit = executorService2.submit(callable);
executorService3.execute(myThread);
}
}
}
运行结果:
上文中使用Executors工具类创建线程,但是《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险;
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize :线程池中允许存在的工作线程的最大数量
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit :keepAliveTime 参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂
handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
ThreadPoolExecutor饱和策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
代码示例:
package 线程池;
import 线程的停止.退出标志停止.MyThread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
Main main = new Main();
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Mythread mythread = main.new Mythread();
//执行Runnable
executor.execute(mythread);
}
}
class Mythread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行");
}
}
}
执行结果:
可以看到我们上面的代码指定了:
corePoolSize: 核心线程数为 5。
maximumPoolSize :最大线程数 10
keepAliveTime : 等待时间为 1L。
unit: 等待时间的单位为 TimeUnit.SECONDS。
workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
handler:饱和策略为 CallerRunsPolicy。