文章目录
进程与线程
进程就是 内存给应用程序分配的空间,也就是运行的程序。
CPU采用时间片轮转的方式运行进程,
上下文切换:当一个时间片结束时,进程还在运行,则暂停这个进程的运行,并且将CPU分配给另外一个进程。
当进程暂停后,它会保存当前的状态(标识和资源),在下次切换回来根据之前的状态进行恢复,继续执行。
相比多进程实现并发,多线程有更多的优势:
- 线程间通信更加简单,它们共享这资源,这些资源在线程间通信比较容易。
- 进程是重量级的,而线程是轻量级的,开销相比更小
进程和线程的区别
本质区别是两者是否单独拥有着地址空间和其他系统资源
- 进程独占地址空间在内存隔离,通信困难,同步方便,稳定性强(即使出现问题,不会影响系统)。
- 线程共享着地址空间和资源,同步困难,通信方便,稳定性不强(如果出现崩溃,则会影响到整个程序的稳定性)。
进程是资源分配的单元,而线程是OS进行调度的单位即CPU分配时间的单位。
剖析 Thread 方法
Thread 类是⼀个 Runnable 接⼝的实现类,我们来看看 Thread 类的源码。
查看 Thread 类的构造方法,发现其实是简单调用⼀个私有的 init 方法来实现初
始化。
init 的方法签名:
// Thread类源码
// ⽚段1 - init⽅法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
// ⽚段2 - 构造函数调⽤init⽅法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// ⽚段3 - 使⽤在init⽅法⾥初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// ⽚段4 - 两个对⽤于⽀持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
init 方法的参数:
- g :线程组,指定这个所创建的线程是在哪个线程组下的
- target:指定要执行的任务
- name:线程名字
- acc:用于初始化私有变量 inheritAccessControlContext (有待考究)
- inheritThreadLocals:可继承的 TheadLocal (重点)
Thread 类的常用方法
- currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;
- start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;
- yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿
意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield()
⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的; - sleep():静态⽅法,使当前线程睡眠⼀段时间;
- join():使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是
Object类的wait⽅法实现的;
继承 Thread 和 实现 Runnable 两者比较
灵活 - 单独 - 低耦合 - 轻量
- 由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。
- Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。
- Runnable接⼝出现,降低了线程对象和线程任务的耦合性。
- 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更
为轻量。
Callable、Future 和 FutureTask
使用 Runnable 和 Thread 是创建线程,唯一弊端是它们是没有返回值的。
JDK提供了 Callable 接口和 Future 类通过"异步"模型来实现。
Callable 接口
Callable 与 Runnable 类似,同样是只有⼀个抽象⽅法的函数式接⼝。不同的
是, Callable 提供的⽅法是有返回值的,⽽且⽀持泛型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable 接口一般配合着线程池工具 ExecutorService 的 submit() 来返回一个 Future 类,再调用 get() 来获取返回值。
// ⾃定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要⼀秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使⽤
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调⽤get⽅法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。
System.out.println(result.get());
}
}
Future 接口
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel 方法是试图取消一个线程。
注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成功”的意思。
参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。
所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable 。
FutureTask 类
上面所说的 Future 接口的实现类就是 FutureTask
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
Future 只是一个接口,我们还需要实现其他的 cancel,get,isDone 方法是非常复杂的
// ⾃定义Callable,与上⾯⼀样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要⼀秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使⽤
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
在很多⾼并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能
够在⾼并发环境下确保任务只执⾏⼀次。
线程组和线程优先级
线程组
每个Thread必然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。
执⾏main()⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程
(当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组
/**
* @Author Juniors Lee
* @Date 2021/11/15
*/
public class ThreadGroup {
public static void main(String[] args) {
new Thread(()->{
System.out.println("当前线程组的名字:"+Thread.currentThread().getThreadGroup().getName());
System.out.println("当前线程的名字:"+Thread.currentThread().getName());
},"A").start();
System.out.println("执行main方法的线程的名字"+Thread.currentThread().getName());
}
}
输出结果:
执行main方法的线程的名字:main
当前线程组的名字:main
当前线程的名字:A
ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回收
线程优先级
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼)。
Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。
/**
* @Author Juniors Lee
* @Date 2021/11/15
*/
public class ThreadPriority extends Thread{
@Override
public void run() {
System.out.println(String.format("当前线程是%s,优先级是%d",Thread.currentThread().getName(),Thread.currentThread().getPriority()));
}
public static void main(String[] args) {
IntStream.range(1,10).forEach(i ->{
Thread thread = new ThreadPriority();
thread.setPriority(i);
thread.start();
});
}
}
执行结果
当前线程是Thread-7,优先级是8
当前线程是Thread-2,优先级是3
当前线程是Thread-4,优先级是5
当前线程是Thread-6,优先级是7
当前线程是Thread-1,优先级是2
当前线程是Thread-8,优先级是9
当前线程是Thread-3,优先级是4
当前线程是Thread-0,优先级是1
当前线程是Thread-5,优先级是6
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的线程调度算法决定的。
在之前,我们有谈到⼀个线程必然存在于⼀个线程组中,那么当线程和线程组的优先级不⼀致的时候将会怎样呢?
我们⽤下⾯的案例来验证⼀下:
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("t1");
threadGroup.setMaxPriority(6);
Thread thread = new Thread(threadGroup,"thread");
thread.setPriority(9);
System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());
System.out.println("我是线程的优先级"+thread.getPriority());
}
运行结果:
我是线程组的优先级6
我是线程的优先级6
所以,如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取⽽代之的是线程组的最⼤优先级。
复制线程组
// 复制⼀个线程数组到⼀个线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
TheadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);
线程状态
Java线程的6个状态:
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW
处于NEW状态的线程,表明还没启动,即没有调用start().
其中关于 start() 的两个问题:
- 反复调⽤同⼀个线程的start()⽅法是否可⾏?
- 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程
的start()⽅法是否可⾏?
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
debug我们发现
- 第⼀次调⽤start()时threadStatus的值是0。
- 第⼆次调⽤start()时threadStatus的值不为0。
因此值不为0直接抛出异常
RUNNABLE
即正在运行
BLOCKED
即阻塞状态。
处于该状态的线程正在等待锁的释放以进入同步区。
WAITING
即等待状态。
处于等待状态转入运行状态的线程需要其他线程的唤醒。
调用以下方法会进入等待模式:
- Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;
- Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait()⽅法;
- LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度
TIME_WAITING
超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。
调⽤如下⽅法会使线程进⼊超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过
notify()/notifyAll()唤醒; - Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则
会⼀直执⾏;
TERMINATED
线程状态的转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFzREfCt-1636974985543)(C:\Users\Juniors Lee\Pictures\MySQL\微信截图_20211115162616.png)]
/**
* @Author Juniors Lee
* @Date 2021/11/15
*/
public class RunnableBlocked {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(RunnableBlocked::func,"A");
Thread b = new Thread(RunnableBlocked::func,"B");
Thread c = new Thread(RunnableBlocked::func,"B");
c.start();
b.start();
a.start();
c.join(1500);
//Thread.sleep(1000);
System.out.println("A:"+a.getState());
System.out.println("B:"+b.getState());
System.out.println("C:"+c.getState());
}
private static synchronized void func(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
调整 func 函数的睡眠时间 或者 main 函数的睡眠时间
线程中断
在某些情况下,我们在线程启动后发现并不需要它继续执⾏下去时,需要中
断线程。⽬前在Java⾥还没有安全直接的⽅法来停⽌线程,但是Java提供了
线程中断机制来处理需要中断线程的情况。
线程中断机制是⼀种协作机制。需要注意,通过中断操作并不能直接终⽌⼀
个线程,⽽是通知需要被中断的线程⾃⾏处理。
几个中断方法:
-
Thread.interrupt():中断线程。这⾥的中断线程并不会⽴即停⽌线程,
⽽是设置线程的中断状态为true(默认是flase);
-
Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个⽅法的影响,
意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这个线程的中断状态重新转为false;
-
Thread.isInterrupted():测试当前线程是否被中断。
与上⾯⽅法不同的是调⽤这个⽅法并不会影响线程的中断状态。
中断但为完全中断:
在线程中断机制⾥,当其他线程通知需要被中断的线程后,线程中断的状态
被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程
⾃⼰⽽定,可以在合适的实际处理中断请求,也可以完全不处理继续执⾏下
去。
线程间通信
Synchronized
通过对一个对象加锁,来实现线程的同步。
但这样的加锁方式,
线程要不断的区尝试获得锁,如果失败了,就会再次去尝试,因而会浪费CPU资源。
等待/通知机制
因而引入了基于Object类中的 notify/notifyAll 来实现多线程同步机制。
信号量
信号量就是一种基于 voliate 关键字的自己实现的通信。
volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥
需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操
作,所以我们需要使⽤ synchronized 给它“上锁”。
管道
管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、PipedOutputStream 、 PipedInputStream 。
其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。
我们通过线程的构造函数,传⼊了 PipedWrite 和 PipedReader 对象。可以简单分析
⼀下这个示例代码的执⾏流程:
- 线程ReaderThread开始执⾏,
- 线程ReaderThread使⽤管道reader.read()进⼊”阻塞“,
- 线程WriterThread开始执⾏。
- 线程WriterThread⽤writer.write(“test”)往管道写⼊字符串,
- 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕,
- 线程ReaderThread接受到管道输出的字符串并打印,
- 线程ReaderThread执⾏完毕。
管道的应用场景:
使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。
通信相关的方法
Join
它的作用是让线程陷入"等待"状态,等join这个线程执行完成之后,再继续执行当前线程。
main会等thread完成后执行。
Sleep
Sleep不会释放当前的锁,而wait方法会。
sleep 与 wait 的区别:
- wait可以指定时间,也可以不指定;⽽sleep必须指定时间。
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易
死锁。 - wait必须放在同步块或同步⽅法中,⽽sleep可以再任意位置
ThreadLocal
TheadLocal 是本地副本变量工具类。
ThreadLocal不属于多线程的通信,而是让每个线程都有自己"独立"的变量,线程之间互不影响。
它为每个所属线程都创建了一个副本,每个线程都能访问到自己的副本变量。
最常用的是get,set方法
/**
* @Author Juniors Lee
* @Date 2021/11/15
*/
public class ThreadLocalDemo {
static class ThreadA implements Runnable {
private ThreadLocal<String> threadLocal;
public ThreadA(ThreadLocal threadLocal){
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("Juniors");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadA输出:" + threadLocal.get());
}
static class ThreadB implements Runnable {
private ThreadLocal<String> threadLocal;
public ThreadB(ThreadLocal threadLocal){
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("Kobe");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadB输出:" + threadLocal.get());
}
}
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(new ThreadA(threadLocal)).start();
new Thread(new ThreadB(threadLocal)).start();
}
}
}
如果只是线程隔离,为什么不每个线程都声明一个私有变量呢?
这是为了希望类中的某一个变量能够和线程状态有关联。
最常⻅的ThreadLocal使⽤场景为⽤来解决
数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。
如果在每个线程中声明⼀些私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。
InheritedThreadLocal
Inheritable是继承的意思。
它不仅仅是当前线程可以存取副本值,⽽且它的⼦线程也可以存取这个副本值。