【并发编程笔记】 ---- 基础(一)

本文详细介绍了Java线程的基础概念,包括线程的创建、运行、状态转换、同步机制、中断处理、上下文切换、死锁预防、守护线程与用户线程的区别,以及ThreadLocal的使用和原理。

文章目录

1.1 什么是线程
1.2 线程创建与运行
1.3 wait、notify、notifyAll方法
1.4 join方法
1.5 sleep方法
1.6 yield方法
1.7 线程中断
1.8 线程上下文切换
1.9 线程死锁
1.10 守护线程和用户进程
1.11 ThreadLocal
1.12 线程的四种状态

1.1 什么是线程

线程是进程的一个实体,线程不会独立存在。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。线程是CPU分配的基本单位

在Java中,启动main函数就启动了一个JVM进程,main函数所在的线程就是这个进程中的一个线程,也称主线程。进程和线程的关系如下:


  • 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,每个线程有私有的程序计数器(记录下一条指令的地址)和栈(存储该线程的局部变量和线程的调用栈帧)
  • 堆被进程中的所有线程共享,进程创建时分配,主要存放使用new操作创建的对象实例
  • 方法区存放JVM加载的类、常量及静态变量等信息,也是线程共享

1.2 线程创建与运行

Java中有三种线程创建方式,分别为实现Runnable接口的run方法、继承Thread类并重写run的方法、使用FutureTask方式

实现Runable接口

    public static class RunableTask implements Runnable {
		@Override
		public void run() {
			System.out.println("I am a child thread");
		}
    }
    public static void main(String[] args) {
		RunableTask task = new RunableTask();
		new Thread(task).start(); // 创建线程并开启
	}

继承Thread

// 继承Thread类并重写run方法
	public static class MyThread extends Thread{
		public void run(){
			System.out.println(this);
			System.out.println(Thread.currentThread());
			System.out.println("I am a child thread");
		}
	}
	public static void main(String[] args) {
	       // 创建线程
		    MyThread myThread = new MyThread();
		    // 启动线程
		    myThread.start();
	}
	

使用FutureTask

// 创建任务类,类似Runable
	public static class CallerTask implements Callable<String> {

		@Override
		public String call() throws Exception {
			return "hello";
		}
	}
	public static void main(String[] args) {

		// 创建异步任务
		FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
		// 启动线程
		new Thread(futureTask).start();

		// 等待任务执行完毕,并返回结果
		try {
			String result = futureTask.get();
			System.out.println(result);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}

1.3 wait、notify、notifyAll(线程等待与通知)

wait函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,只有通过其他线程调用了该共享变量的notify()或者notifyAll()方法,或者其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。

notifyAll()函数
notifyAll()会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程

1.4 join方法(等待线程执行终止)

join方法: 等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕后汇总处理。

1.5 sleep方法(让线程睡眠)

Thread静态sleep方法,调用线程会暂时让出指定时间的执行权,不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回

线程在睡眠时拥有的监视器资源不会被释放

1.6 yied方法(让出CPU执行权)

Thread静态yield方法,当一个线程调用yield方法时,暗示线程调度器当前线程请求让出自己的CPU使用

sleep与yield方法的区别:

  • 当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。
  • 当调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调用时就有可能调度到当前线程执行

1.7 线程中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理

  • void interrupt()方法: 中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true时并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用
    线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回
  • boolean isInterrupted()方法: 检测当前线程是否被中断,如果是返回true,否则返回false
  • boolean interrupted(): 检测当前线程是否被中断,如果是返回true,否则返回false。该方法发现当前线程被中断,则会清除中断标志
public class InterruptedTest {
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				// 如果当前线程被中断则退出循环
				while(!Thread.currentThread().isInterrupted()) {
					System.out.println(Thread.currentThread() + " hello");
				}
			}
		});

		// 启动子线程
		thread.start();

		// 主线程休眠1s,以便中断前让子线程输出
		Thread.sleep(1000);

		// 中断子线程
		System.out.println("main thread interrupt thread");
		thread.interrupt();
		
		// 等待子线程执行完毕
		thread.join();
		System.out.println("main is over");
	}
}

在这里插入图片描述

1.8 线程上下文切换

多线程编程中,线程个数一般都大于CPU个数,每个CPU同一时刻只能被一个线程使用,CPU资源的分配采用了时间片轮转的策略,给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。在切换线程上下文时需要保存当前线程的执行现场,当下次轮转到自己占有CPU时就能直到自己之前运行到哪里了

1.9 线程死锁

1.9.1 什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

图中,线程A已经持有了线程2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。

死锁产生必须具备以下四个条件

  • 互斥条件: 指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。
  • 请求并持有条件: 指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  • 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源
  • 环路等待条件: 指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源…Tn正在等待已被T0占用的资源

在这里插入图片描述

1.9.2 如果避免线程死锁
破坏至少一个构成死锁的必要条件即可。目前只有请求并持有环路等待条件是可以被破坏的

造成死锁的原因和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

1.10 守护线程和用户线程

Java中的线程分为两类,分别为daemon线程(守护线程) 和 user线程(用户线程)

在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。

守护线程和非守护线程区别之一: 当最后一个非守护线程结束时,JVM会正常退出,不管当前是否有守护线程。(守护线程是否结束不影响JVM的退出,只要有一个用户线程还没结束,JVM就不会退出)

总结
如果希望在主线程结束后JVM进程马上结束,在创建线程时可以将其设置为守护线程
如果希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程

1.11 ThreadLocal

ThreadLocal由JDK包提供,它提供了线程本地变量,如果你创建了一个ThreadLocal变量,访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

1.11.1 ThreadLocal使用示例

public class ThreadLocalTest {

	// (1) print函数
	static void print(String str) {
		// 1.1 打印当前线程本地内存中的localVariable变量的值
		System.out.println(str + ":" + localVariable.get());
	}

	// (2) 创建ThreadLocal变量
	static ThreadLocal<String> localVariable = new ThreadLocal<>();

	public static void main(String[] args) {
		// (3) 创建线程one
		Thread threadOne = new Thread(new Runnable() {
			@Override
			public void run() {
				// 3.1 设置线程One中本地变量localVariable的值
				localVariable.set("threadOne local variable");
				// 3.2 调用打印函数
				print("threadOne");
				// 3.3 打印本地变量值
				System.out.println("threadOne remove after" + ":" + localVariable.get());
			}
		});

		// (4) 创建线程two
		Thread threadTwo = new Thread(new Runnable() {
			@Override
			public void run() {
				// 4.1 设置线程Two中本地变量localVariable的值
				localVariable.set("threadTwo local variable");
				// 4.2 调用打印函数
				print("threadTwo");
				// 4.3 打印本地变量值
				System.out.println("threadTwo remove after" + ":" + localVariable.get());
			}
		});

		// (5) 启动线程
		threadOne.start();
		threadTwo.start();
	}
}

通过set方法设置了localVariable的值,其实设置的是线程本地内存的一个副本,这个副本其它线程无法访问。
通过get函数获取当前线程本地内存中localVariable的值

1.11.2 ThreadLocal的实现原理
在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为ThreadLocalMap类型,其中的key为我们定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果线程一直不消亡,则本地变量一直存在,所以可能会造成内存溢出,因此使用完毕后要调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。

1.11.3 ThreadLocal不支持继承性

public class TestThreadLocal {

	// (1)创建线程变量
	public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

	public static void main(String[] args) {
		// (2) 设置线程变量
		threadLocal.set("hello world");
		// (3) 启动子线程
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				// (4) 子线程输出线程变量的值
				System.out.println("thread:" + threadLocal.get());
			}
		});

		thread.start();

		// (5) 主线程输出线程变量的值
		System.out.println("main:" + threadLocal.get());
	}
}

在这里插入图片描述

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。因为在子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量是main线程,两者是不同的线程,子线程访问时返回null

1.11.4 InheritableThreadLocal类
InheritableThreadLocal继承自ThreadLocal,提供了一个特性,可以让子线程访问在父线程中设置的本地变量

  1. 什么情况需要子线程可以获取父线程的threadlocal变量?
  • 子线程需要使用存放在threadlocal变量中的用户登录信息
  • 一些中间件需要把统一的id追踪的整个调用链路记录下来
  1. 子线程使用父线程中的threadlocal方法方式
  • 创建线程时传入父线程的变量,并将其复制到子线程中
  • 或者在父线程中构造一个map作为参数传递给子线程

1.12 线程的四种状态

  • 创建(new)状态: 准备一个多线程对象,即执行了new Thread()
  • 就绪(runnable)状态:调用了start()方法,等待CPU进行调度
  • 运行(running)状态:执行run()方法
  • 阻塞(blocked)状态:暂时停止执行线程,将线程挂起
  • 死亡(terminated)状态:线程销毁
    在这里插入图片描述
    在这里插入图片描述
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值