Java线程基础知识
进程与线程
- 进程:一个在内存中运行的应用程序。
- 线程:进程中的一个执行任务,线程在程序里独立执行的。一个进程至少包含一个线程,一个进程可以运行多个线程,多个线程可以共享数据
进程与线程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器调度的基本单位
- 资源开销:每个进程有独立的代码和数据空间,进程之间的切换会有较大的开销。同一类线程共享进程里的代码和数据空间,每个线程都有自己独立的运行栈和程序计数器PC,线程之间切换开销小。
- 包含关系: 一个进程包含多个线程,这些线程共享进程占有的资源和地址空间。
- 内存分配:同一进程的线程共享本线程的数据和地址空间,不同进程之间的数据和地址空间相互独立
并发与并行
- 并发:多个任务在同一个CPU上,按细分的时间片轮流执行,从逻辑上看这些任务是同时执行
- 并行:单位时间内,多个CPU或多核CPU同时处理多个任务,真正意义上的同时执行
线程的创建
- 继承Thread类,Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例
- Thread源码
public class Thread implements Runnable {
private static native void registerNatives();
static {
registerNatives();
}
private volatile String name;
private int priority;
private Thread threadQ;
private long eetop;
.
.
.
复制代码
- 创建例子:
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
复制代码
- 实现Runnable接口,如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口实现线程创建
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run();
public void run() {
if (target != null) {
target.run();
}
}
复制代码
- 实现Callable接口。
Callable接口类似于Runnable,因为他们都是为其实例可能由另一个线程执行的类设计的。然而Runnable是不返回结果的,也不能够抛出被检查的异常。与实现Runnable的区别如下:- 可以有返回值,返回值可以通过futureTask的get()方法获取到
- 可以抛出异常
- 执行的方法不同Runnable是run(),而Callable是call()
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
//适配类FutureTask
FutureTask futureTask = new FutureTask(myThread);
new Thread(futureTask,"A线程").start();
new Thread(futureTask,"B线程").start();//开启两个线程,会有缓存,只执行一次
new Thread(futureTask).start();
Object o = futureTask.get();
System.out.println(o);
}
//泛型是什么类型,接口实现的方法返回值就是什么类型
static class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("使用Callable创建线程");
return "使用Callable创建线程的返回值";
}
}
使用Callable创建线程
使用Callable创建线程的返回值
复制代码
- 基于线程池的方式
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
//线程睡眠,sleep是不释放锁的,一直占有
Thread.sleep(2000);
//执行任务结束,释放锁
System.out.println(Thread.currentThread().getName() + "结束睡眠");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
复制代码
关于线程池的知识点,大家可以查看我上一篇文章,线程池详解
线程的生命周期
public enum State {
#新生
NEW,
#运行
RUNNABLE,
#阻塞
BLOCKED,
#等待
WAITING,
#超时等待
TIMED_WAITING,
#终止
TERMINATED;
}
复制代码
1. New 初始状态,线程被创建时的状态,即通过new关键字创建的一个新的线程对象。
2. Runnable 运行状态,Java将线程的就绪状态和运行状态统称为运行态。
3. Blocked 阻塞状态,阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞(o.wait->等待队列):运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞(lock->锁池): 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
- 其他阻塞(sleep/join) : 运行(running)的线程执行Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
4. Waiting 等待状态,进入该状态的线程需要等待其他线程做出一些特定的动作,如线程通知或线程中断,必须被唤醒才能有机会执行,否则处于等待状态的线程将无限期等待下去。
5. Timed_Waiting 超时等待状态,超时等待不同于等待状态,处于超时等待状态的线程可以在指定的时间后自动唤醒
6. Terminated 终止状态,线程执行完需要执行的任务后,线程处于终止状态,
- 线程状态转换如下图:
sleep 与 wait 区别
-
对于sleep()方法,我们首先要知道该方法是属于Thread 类中的。而 wait()方法,则是属于Object类中的。
-
sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。
-
在调用 sleep()方法的过程中,线程不会释放对象锁。
-
而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入就绪状态。
-
wait()方法可以通过interrupt()方法打断线程的等待状态,线程立即抛出中断异常。
-
调用wait()方法后重获锁对象的方式
- 设置wait()的参数,自动回收锁。
- 让其他线程通过notify()/notifyAll()方法唤醒等待的线程。
start 与 run 区别
- start()方法是用来启动线程,使线程从初始态进入就绪态。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用 Thread 类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,当cpu调度执行到当前线程,当前线程就会进入运行状态,开始运行run函数当中的代码。Run 方法运行结束, 此线程终止,然后 CPU 再调度其它线程。
线程同步以及线程调度的相关方法
- sleep() : 使一个处于运行态的线程处于睡眠状态,不释放锁
- wait() : 是一个线程处于等待状态,并释放所持有的锁对象。
- notify() : 唤醒一个处于等待状态的线程,该方法并不能唤醒一个指定的线程,而是由JVM决定具体唤醒哪一个,而且与优先级无关。
- notifyAll() : 唤醒所有处于等待状态的所有线程,该方法并不是将对象锁给所有线程,而是让他们去竞争,只有竞争得到锁对象的线程才能进入就绪态,等待cpu调度。
如何唤醒一个阻塞的线程
- 首先,wait(),notify()是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放锁对象,相应地,调用任意一个对象的notify()方法则将随机解除该对象阻塞的线程,但被解除的线程需要重新获得此锁对象,才能继续往下执行。
- 其次,wait(),notify()方法必须在synchronize方法或者synchronize代码块中被调用,并且要保证同步块或方法的锁对象与调用wait()、notify()方法的对象是同一个,如此一来在调用wait()之前当前线程就已经获取了某对象的锁,执行wait()后阻塞当前线程并释放之前获取的锁对象。
notify()和notifyAll()的区别
-
如果线程调用了对象的wait()方法,那么线程便会进入与该对象监视器关联的等待池中,等待池中的线程不会去竞争该锁对象。
-
notifyAll()会唤醒所有线程,notify()只会唤醒一个线程,并且notify()唤醒的线程是由JVM决定的。
-
调用notifyAll()后,会将全部线程由等待池转移到锁池,然后参与锁的竞争,竞争成功获得锁后,继续执行,竞争失败则留在锁池中等待锁再次被释放后再参与竞争。
-
当线程被notify()方法唤醒后,线程将由等待池转移到锁池,获得锁后,线程将回到wait()方法结束的地方继续执行。
-
notify()、notifyAll()、wait()案例
package com.gjy.demo.Thread;
/**
* @Author GJY
* @Date 2021/6/26 10:36
* @Version 1.0
*/
public class WaitNotify1 {
public static void main(String[] args) throws InterruptedException {
//new一个锁对象
Object lock = new Object();
//创建5个wait线程
for (int i = 0; i < 5; i++) {
new WaitMethod(i+"",lock).start();
}
//主线程休眠2秒
Thread.sleep(2000);
System.out.println("主线程休眠2秒");
//启动notifyAll线程唤醒等待池中的线程
new NotifyMethod(lock).start();
}
static class WaitMethod extends Thread{
//锁对象
final Object lock;
WaitMethod(String name,Object lock) {
this.lock = lock;
setName("WaitMethod-"+name);
}
@Override
public void run() {
System.out.println("线程" + getName() + "执行wait方法之前");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被notifyAll唤醒并继续执行wait()方法后的内容
System.out.println("线程" + getName() + "执行wait方法之后");
}
}
//notifyAll()方法
static class NotifyAllMethod extends Thread{
//锁对象
final Object lock;
NotifyAllMethod(Object lock) {
this.lock = lock;
this.setName("NotifyAllThread");
}
@Override
public void run() {
synchronized (lock){
System.out.println("线程"+getName()+"调用notifyAll()方法之前");
try {
//休眠5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒所有线程,将与锁对象关联的等待池中的所有线程转移到与锁对象关联的锁池中,让他们们在锁池中竞争获取lock锁对象
lock.notifyAll();
try {
//休眠5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+getName()+"调用notifyAll()方法之后");
}
}
}
//notify()方法
static class NotifyMethod extends Thread{
//锁对象
final Object lock;
NotifyMethod(Object lock) {
this.lock = lock;
this.setName("NotifyThread");
}
@Override
public void run() {
synchronized (lock){
System.out.println("线程"+getName()+"调用notify()方法之前");
try {
//休眠5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒所有线程,将与锁对象关联的等待池中的所有线程转移到与锁对象关联的锁池中,让他们们在锁池中竞争获取lock锁对象
lock.notify();
try {
//休眠5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+getName()+"调用notify()方法之后");
}
}
}
}
复制代码
使用notify()唤醒线程程序输出:
线程WaitMethod-0执行wait方法之前
线程WaitMethod-4执行wait方法之前
线程WaitMethod-3执行wait方法之前
线程WaitMethod-2执行wait方法之前
线程WaitMethod-1执行wait方法之前
主线程休眠2秒
线程NotifyThread调用notify()方法之前
线程NotifyThread调用notify()方法之后
线程WaitMethod-0执行wait方法之后
使用notifyAll()唤醒线程程序输出:
线程WaitMethod-2执行wait方法之前
线程WaitMethod-3执行wait方法之前
线程WaitMethod-0执行wait方法之前
线程WaitMethod-1执行wait方法之前
线程WaitMethod-4执行wait方法之前
主线程休眠2秒
线程NotifyAllThread调用notifyAll()方法之前
线程NotifyAllThread调用notifyAll()方法之后
线程WaitMethod-4执行wait方法之后
线程WaitMethod-1执行wait方法之后
线程WaitMethod-2执行wait方法之后
线程WaitMethod-0执行wait方法之后
线程WaitMethod-3执行wait方法之后
复制代码
yield()方法
yield()方法用于使当前的线程让出CPU的使用权,使当前线程从运行态转换到就绪态,但不能保证其他线程一定可以获取到CPU执行权,因为当前线程调用yield()方法后处于就绪态仍然具有竞争CPU执行权的权利。
join()方法
join()方法使一个线程在另一个线程执行结束后执行。如果线程B调用了线程A的join()方法,那么线程B将阻塞直到线程A执行完成后才能执行。
- 例子
package com.gjy.demo.Thread;
/**
* @Author GJY
* @Date 2021/6/26 14:37
* @Version 1.0
*/
public class JoinDemo {
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A启动");
try {
System.out.println("线程A睡眠2秒");
Thread.sleep(5000);
System.out.println("线程A执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程A");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
//线程B启动
System.out.println("线程B启动");
//线程B执行线程A的join()方法,那么线程B将阻塞,直到线程A执行完毕
threadA.join();
System.out.println("线程A执行结束,线程B结束阻塞、继续运行");
System.out.println("线程B继续运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程B");
//启动两个线程
threadA.start();
threadB.start();
}
}
复制代码
- 运行结果:
线程A启动
线程A睡眠2秒
线程B启动
线程A执行结束
线程A执行结束,线程B结束阻塞
线程B继续运行
复制代码
线程中断
中断是线程的一个标识位属性,中断线程并不会立刻使线程退出运行。中断是只通知而不是强制强制使线程退出。例如,当你在下载一个文件时,此时会开一个线程来执行下载这个任务,当这个文件还没有下载完成你就想停止下载,此时就可以中断下载文件这个线程,告诉线程我不想下载这个文件了。
- interrupt()中断线程、interrupted()检测线程状态,会清除线程中断状态
public class InterruptDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
//通过interrupted()方法判断线程是否被中断
System.out.println(thread.getName()+"线程-是否中断:"+thread.interrupted());
//设置当前线程中断
Thread.currentThread().interrupt();
//通过interrupted()方法判断线程是否被中断,会清除线程状态
System.out.println(thread.getName()+"线程-是否中断:"+thread.interrupted());//返回true
//检测interrupted()方法是否会清除线程状态---->会
System.out.println(thread.getName()+"线程-是否中断:"+thread.interrupted()); //返回false
}
}
复制代码
- interrupt()中断线程、isInterrupted()检测线程状态,不会清除线程中断状态
public class InterruptDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
//通过interrupted()方法判断线程是否被中断
System.out.println(thread.getName()+"线程-是否中断:"+thread.interrupted());
//设置当前线程中断
Thread.currentThread().interrupt();
//通过isInterrupted()方法判断线程是否被中断,不会清除线程状态
System.out.println(thread.getName()+"线程-是否中断:"+thread.isInterrupted());//返回true
//检测isInterrupted()方法是否会清除线程状态---->不会
System.out.println(thread.getName()+"线程-是否中断:"+thread.isInterrupted()); //返回true
}
}
复制代码
- interrupted() 方法,是Thread中的一个静态方法,其中内部是调用isInterrupted()方法,传入true,表示清除线程状态
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
复制代码
- isInterrupted()方法,是Thread的一个实例方法,返回调用该方法的对象的线程的中断状态,不会清除线程状态。
public boolean isInterrupted() {
return isInterrupted(false);
}
复制代码
线程类的构造方法、静态代码块是被哪个线程调用的?
线程类的构造方法、静态代码块是被new这个线程类所在的线程所调用的,而run()方法里面才是被线程自身所调用的。例如,线程A里面新建了一个线程B,那么线程B的构造方法、静态代码块就是被线程A所调用的,run()方法就是线程B自己调用的。
- end