Java线程基础
往期推荐
1. 进程与线程
- 进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 线程:系统分配处理器时间资源的基本单元,是进程中的一个执行任务,线程在程序里独立执行的。一个进程至少包含一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
2. 进程与线程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 资源开销:每个进程有独立的代码和数据空间(程序上下文),进程之间的切换会有较大的开销。同一类线程共享进程里的代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换开销小。
- 包含关系:一个进程包含多个线程,这些线程共享进程占有的资源和地址空间。
- 内存分配:同一进程的线程共享本线程的数据和地址空间,不同进程之间的数据和地址空间相互独立。
- 通信方式:进程之间可以通过管道、消息队列、共享内存、信号量,以及Socket等机制实现通信,线程之间主要通过共享变量及其变种形式实现通信。
- 影响关系:子进程无法控制父进程,一个进程发生异常时一般不会影响其他进程;子线程可以控制父线程,如果主线程发生异常,会影响其所在进程和其余线程。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
3. 并发与并行
- 并发:多个任务在同一个CPU上,按细分的时间片轮流执行,从逻辑上看这些任务是同时执行。
- 并行:单位时间内,多个CPU或多核CPU同时处理多个任务,真正意义上的同时执行。
4. 创建线程的四种方式
- 继承
Thread类 - 实现
Runnable接口 - 实现
Callable接口 - 使用
Executors工具类创建线程池
通过继承 Thread 类创建线程的步骤:
- 定义
Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。 - 创建
Thread子类的实例,即创建了线程对象。 - 调用线程对象的
start()方法来启动该线程。
- 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 ThreadTest extends Thread {
private int i;
@Override
public void run() {
for(; i < 2; i++) {
System.out.println("继承Thread启动线程:" + getName() + " : " + i);
}
setName("Thread-new");
for(; i < 4; i++) {
System.out.println("重命名后的新线程名:" + Thread.currentThread().getName() + " : " + i);
}
}
public static void main(String[] args) {
System.out.println("main线程:" +Thread.currentThread().getName());
new ThreadTest().start();
}
}
通过实现 Runnable 接口创建线程的步骤:
- 定义
Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。 - 创建
Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。 - 调用线程对象的
start()方法来启动该线程。
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接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。 - 使用
FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。 - 使用
FutureTask对象作为Thread对象的target创建并启动新线程。 - 调用
FutureTask对象的get()方法来获得子线程执行结束后的返回值。
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创建线程的返回值";
}
}
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
- 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
- 优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现
Runnable接口、Callable接口的方式来创建多线程。
使用 Executors 工具类创建线程池
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
//线程睡眠,sleep是不释放锁的,一直占有
Thread.sleep(3000);
//执行任务结束,释放锁
System.out.println(Thread.currentThread().getName() + "结束睡眠");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
5.线程的生命周期
- 线程状态转换如下图:
1. New 初始状态,线程被创建时的状态,即通过new关键字创建的一个新的线程对象。
2. Runnable 运行状态,Java将线程的就绪状态和运行状态统称为运行态。
3. Blocked 阻塞状态, 阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞:在运行状态的线程调用 o.wait 方法时,JVM 会把该线程放入等待队列(Waitting Queue)中,线程转为阻塞状态。
- 同步阻塞:在运行状态的线程尝试获取正在被其他线程占用的对象同步锁时,JVM 会把该线程放入锁池(Lock Pool)中,此时线程转为阻塞状态。
- 其他阻塞:运行状态的线程在执行 Thread.sleep(long ms)、Thread.join()或者发出I/O 请求时,JVM 会把该线程转为阻塞状态。直到 sleep()状态超时、Thread.join()等待线程终止或超时,或者 I/O 处理完毕,线程才重新转为可运行状态。 4. Waiting 等待状态,进入该状态的线程需要等待其他线程做出一些特定的动作,如线程通知或线程中断,必须被唤醒才能有机会执行,否则处于等待状态的线程将无限期等待下去。
5. Timed_Waiting 超时等待状态,超时等待不同于等待状态,处于超时等待状态的线程可以在指定的时间后自动唤醒
6. Terminated 终止状态,线程执行完需要执行的任务后,线程处于终止状态。
6.sleep()与wait()的区别
-
sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
-
wait()、notify()、notifyAll()只能在同步方法或者同步代码块中使用,而sleep可以在任何地方使用。
-
sleep()不会释放锁,而wait()会释放锁,并需要通过notify()/notifyAll()重新获取锁。
-
wait()通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执行。
-
sleep()方法必须捕获异常,在调用sleep()方法的过程中有可能会被其他对象调用它的interrupt(),从而产生InterruptedException异常,如果程序不捕获这个异常,线程就会异常终止,进入Terminated状态(终止态);而wait()、notify()、notifyAll()不需要捕获异常。wait()方法可以通过interrupt()方法打断线程的等待状态,线程立即抛出中断异常。
7.线程的 sleep()方法和 yield()方法有什么区别?
(1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2)线程执行 sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
8.线程同步以及线程调度的相关方法
sleep(): 使一个处于运行态的线程处于睡眠状态,不释放锁wait(): 是一个线程处于等待状态,并释放所持有的锁对象。notify(): 唤醒一个处于等待状态的线程,该方法并不能唤醒一个指定的线程,而是由JVM决定具体唤醒哪一个,而且与优先级无关。notifyAll(): 唤醒所有处于等待状态的所有线程,该方法并不是将对象锁给所有线程,而是让他们去竞争,只有竞争得到锁对象的线程才能进入就绪态,等待cpu调度。
9.如何停止一个正在运行的线程?
-
使用退出标志,使线程正常退出,也就是当
run()方法完成后线程终止。 -
使用
stop()方法强行终止,但是不推荐这个方法,因为stop()和suspend()及resume()一样都是过期作废的方法。 -
使用
interrupt()方法中断线程。
10.如何唤醒一个阻塞的线程?
- 首先,wait(),notify()是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放锁对象,相应地,调用任意一个对象的notify()方法则将随机解除该对象阻塞的线程,但被解除的线程需要重新获得此锁对象,才能继续往下执行。
- 其次,wait(),notify()方法必须在synchronize方法或者synchronize代码块中被调用,并且要保证同步块或方法的锁对象与调用wait()、notify()方法的对象是同一个,如此一来在调用wait()之前当前线程就已经获取了某对象的锁,执行wait()后阻塞当前线程并释放之前获取的锁对象。
11.stop()和suspend()方法为何不推荐使用
- stop 这个方法将终止所有未结束的方法,包括run方法。当线程想终止另一个线程的时候,它无法知道何时调用stop是安全的,何时会导致对象被破坏。所以这个方法被弃用了。你应该中断一个线程而不是停止他。
- suspend 被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会破坏对象强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在获得相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。
12.start 与 run 区别
- start()方法是用来启动线程,使线程从初始态进入就绪态。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用 Thread 类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,当cpu调度执行到当前线程,当前线程就会进入运行状态,开始运行run函数当中的代码。Run 方法运行结束, 此线程终止,然后 CPU 再调度其它线程。
13.yieId()、join()
yieId()
用于使当前的线程让出CPU的使用权,使当前线程从运行中的状态切换到可运行状态,但不保证其他线程一定可以获得CPU执行权,因为当前线程执行了yield()方法后变为可运行状态,此时当前线程依然有机会获取CPU执行权。
join()把指定的线程加入到当前线程,让一个线程等待另一个线程执行完以后再执行,可以将两个交替执行的线程合并为顺序执行的线程。
...
try {
threadA.start();
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前线程等待threadA线程执行完以后,再往下执行
...
14.如何实现线程同步?
1. 同步方法
即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2. 同步代码块
即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3. ReentrantLock
Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。
4. volatile
volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
5. 原子变量
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
15.notify()、notifyAll()的区别
- notify()
用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
- notifyAll()
用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
16.线程中断
中断是线程的一个标识位属性,表示一个运行中的线程是否被其他线程进行了中断。中断只是通知而不是强制线程退出。
- 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
}
}
17.interrupted() 和 isInterrupted()的区别
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
必须注意的是:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted :是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted :查看当前中断信号是true还是false。
18.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new⼀个Thread,线程进⼊了新建状态。调⽤start()⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。start()会执⾏线程的相应准备⼯作,然后⾃动执⾏run()⽅法的内容,这是真正的多线程工作。 但是,直接执⾏run()⽅法,会把run()⽅法当成⼀个main线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏。