文章目录
概述
在Java中,程序的运行可以笼统地分为两种方式:同步
,异步
。
假设我们在某个方法里,前后有两个子方法:method1
、method2
,现在想要执行完这两个子方法,按照同步、异步的方式,有以下两种执行过程:
- 同步:先执行完成
method1
后,才会执行method2
。即想要执行method2
的话,必须先执行完成method1
,且中途不能出错,否则程序就会终止,也就执行不到method2
了。
对于同步来说,程序就是线性的,根据代码编写的顺序自上而下执行。想要执行某一步,就必须经过前面的代码,无法跳跃。如果其中某一步报错了,后面的也就无法继续。如果前面耗费时间很长,后面的方法也只能是等待,等待前面的执行完。 - 异步:将
method1
、method2
分别作为两个异步方法执行。这时候,这两个方法就是各自独立、互不干扰、可以同时执行的。即想要执行method2
时,再也不需要关注method1
是否执行完毕,是否发生报错了。
对于异步,就是指脱离了程序的线性执行,可以在任何需要执行的时候执行的方式。异步的程序,互相独立、各自运行而不互相干扰。
本章讲的多线程,就是java中异步的具体体现。
我们平时说的程序,在系统上的呈现出来的概念就是进程
。而线程
,就是每个进程
中的独立运行的任务。
虽然多线程是异步、可以同时执行的。但是,这里的“同时”,其实只是偷换概念,确切来说是指在用户察觉不到的情况下的“同时”,并不是真正意义上的同一时间进行。
因为在操作系统上,一个cpu同一时间,只能有一个程序在执行。所以多线程的“同时”,其实是这个cpu为每条线程分配时间片,在极短的时间内,根据各自的时间片,不断地进行程序切换运行,所达到的效果。切换的时间足够小,比如10ms可以对10条线程都切换一遍,这样用户就很难觉察得到这之间的时延,就产生了“同时”的感受。
注:
现在计算机大多都是多核处理器了,也就意味着,多线程的性能得到极大的提升。
如果计算机的内核数不小于程序的线程数,那每个cpu就不需要为多线程分配时间片,就可以直接单独处理一条线程。
这样,多个cpu同时运作,每个cpu的线程也在同时运作,就能达到真正意义上的“同时”了。
简而言之,多线程能为我们的程序带来更多的创造性,是我们在开发中必不可少的一个技能。
平时对多线程这一块做了一些积累,现在放上来,分享、回顾、互相学习。
1、多线程的4种创建方式
- 继承
Thread
类,重写run()
方法,调用start()
方法开启线程。
//1、继承Thread类,重写 run() 方法
public class MyThread extends Thread{
public void run(){
//线程执行的内容
System.out.println("mythread run");
}
}
//2、在需要的地方调用 start() 方法执行
MyThread t =new MyThread();
t.start();
- 实现
Runnable
接口并编写run()
方法。
//1、实现 Runnable 接口,写 run() 方法
public class MyRunnable implements Runnable{
public void run(){
//线程执行的内容
System.out.println("myrunnable run");
}
}
//2、新建Thread的时候,用runnable实现类来构建Thread
Thread t = new Thread(runnable);
t.start();
- 实现
Callable
接口并编写run()
方法。
Callable
与Runnable
的不同之处是,有返回值。
返回值为Future
对象,在Future
对象上调用get
方法就可以获取Callable
任务返回的Object
了。
//1、实现 Callable 接口,写 call() 方法
public class MyCallable implements Callable {
public String call() {
String str = "mycallable call";
return str;
}
}
//2、用Callable对象,构建FutureTask
FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
//3、用FutureTask对象,构建Thread,并开启线程
new Thread(futureTask).start();
//4、等线程执行完成后,获取结果
String str = futureTask.get();
- 使用
线程池
创建。
后续介绍
2、线程锁机制简介
为了使后文通畅,更易理解,先简单介绍下锁机制,后续再补充完整这一块。
多线程在一般情况下,是异步运行,互不干扰的。但有时候线程会使用到公共的资源,由于线程又是不同步的,多个线程使用了同个资源,势必就会造成资源的不同步。即:A线程和B线程获取了同一个资源,此时A线程对资源进行写操作,B线程进行读操作。等A线程写操作完成,B线程读到的还是一开始获取到的资源,并不是A线程更新后的新的资源,这就造成了脏读。
类似的情况还有很多,只要是对共享内容有操作(如对公共资源的读写),多线程的异步就会出现问题。
因为共享内容只有一份,而多线程又是各自独立异步的。所以要解决问题,就需要加入一定的机制。使多线程在操作共享内容的时候是同步的,其他时候是异步的。
所以锁机制就产生了。
为每个共享资源对象配置一个锁对象。想访问它的话,必须先取得锁,访问完成后,必须释放锁。
由于每个资源对应的锁只有一个。这样,每个线程在访问这个资源对象的时候,同一时间就只有一个线程能获取到锁访问到资源,而其他线程必须等待这个锁释放出来后,才能争取到这个锁,才能访问到这个资源。
这样就可以控制同一时间访问共享资源的线程只有一个,保证了资源的操作是同步的。
一个简单的使用锁的例子,加深下印象:
try {
synchronized(classA){//1、获取锁 classA对象
//2、执行逻辑。。。
//3、执行完成后,自动释放锁 classA对象
}
} catch (Exception e) {
}
3、多线程的生命周期
线程创建并启动后,并不是马上就开始执行。
就如前面说的,一个cpu同一时间只能处理一个线程。所以线程启动后,需要等待cpu分配时间给他,线程才能执行。这其中有多个线程,就需要排队、等待分配时间执行、线程之间的切换,等等。
整理一下以上的逻辑,就有了线程的生命周期。
在一个完整的线程生命周期里,需要经过:新建—>就绪—>运行—>阻塞—>死亡
一共5个状态。
- 新建:就是
new
了一个Thread
对象,此时还未执行start()
方法. - 就绪:当
Thread
对象执行了start()
方法后,线程就处于了就绪状态。即告诉 JVM “我已经准备好了,随时可以来分配时间片给我,让我执行”。此时 JVM 就会为它创建一些基本的调用前的准备,如创建方法调用栈、程序计数器等,等待调用运。 - 运行:处于就绪状态的线程分配到了cpu的时间片,得到了cpu的调用,开始运行其
run()
方法(或call()
方法),此时就是处于了运行状态。 - 阻塞:当线程对象遇到了某些情况后,放弃了cpu当前的使用权,退出来让给其他线程对象执行。此时本线程对象就处于了阻塞状态。处于阻塞状态的线程,需要等待某个时机(具体看导致阻塞的原因),重新进入就绪状态,等待cpu的调用,而不是直接恢复执行状态。(就好比在银行窗口办事时,中途有事离开了,回来之后就要重新排队等待叫号了)
造成阻塞的情况,可以粗略分为三种:- 等待阻塞:调用了该线程的
wait()
方法,线程进入等待队列。 - 同步阻塞:在线程用到某个共享资源,获取其锁的时候,锁被其他线程对象占用,则本线程进入阻塞状态,等待其他线程释放锁,直到获取到锁为止。
- 其他阻塞:调用
sleep()
方法或join()
方法,或者发出来I/O请求的时候,线程会处于阻塞状态。
- 等待阻塞:调用了该线程的
- 死亡:在线程执行结束后,就进入了死亡状态。结束的具体情况分为三种:
- 正常结束:
run()
或call()
方法执行完毕,线程正常结束。(占用的锁会释放) - 异常结束:线程抛出了异常,导致异常结束。
- 调用stop:调用了线程的
stop()
方法,结束了线程的执行。(已过时的方法,线程不安全,不推荐)
- 正常结束:
注:
线程是JVM级别的,所以只有当JVM退出了,未执行完的线程才会真正退出。
如果是在类似Tomcat容器中运行有多线程的程序,就算关闭了Tomcat,还未执行完的线程依旧是活跃的。
4、常用方法—运行
start()
:启动线程的方法,将线程由新建状态改为就绪状态,正式进入等待队列,等待cpu调用线程执行。
注:线程对象新建完成后,并不会自动开启,需要手动调用start()
方法,线程才会正式启动,否则线程对象就仅仅只是一个普通的对象而已。run()
:线程要执行的具体内容的方法。cpu调用线程,执行的就是线程的run()
方法。run()
方法执行完毕,线程就结束。
注:不需要手动调用run()
方法(手动调用就成了普通方法调用,不是线程了),这是当线程被cpu调用时,cpu要执行的线程内容,由系统自动调用。只需要在run()
方法里写上我们需要线程执行的操作即可。
5、常用方法—线程休眠、等待与唤醒、让步
sleep()
:线程休眠。Thread
类的静态方法,设置指定的时间,然后在这个指定时间内,暂停当前线程的执行,让出cpu给其他线程使用。当指定时间到了之后,线程又自动恢复执行。
注:- 调用
sleep()
的时候,线程的锁不会释放。 - 只能是休眠当前运行中线程(
this.currentThread()
),即使是其他线程t
调用t.sleep()
,也只会休眠当前线程。
- 调用
wait()
:线程等待。Object
类的方法,调用后暂停当前线程的执行。可设置暂停时间,也可不设置。
注:- 设置了时间,指定时间后会回到队列排队等待。
- 不设置时间,需要调用
notify()
唤醒方法,才能重新回到队列中。 wait()
是与锁相关的,在调用wait()
之前,必须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中使用wait()
。- 调用
wait()
后,当前线程的对象锁会释放。 - 调用不同对象的
wait()
,只会释放该对象自身的锁。譬如:线程A当前有a、b两个不同对象锁。此时调用a.wait()
只会释放a
的锁,b
锁不会被释放。反之,调用b.wait()
只会释放b
锁,a
锁不会释放。
notify()
:线程唤醒。Object
类的方法,调用后唤醒在该对象上等待的线程,恢复为就绪状态。
注:- 调用
notify()
前,也必须获得该对象的对象锁。 - 调用
notify()
,也一样需要在同步方法或同步代码块中调用。 - 调用
notify()
后,需要等到同步方法或同步代码块执行完毕了,才发挥notify()
唤醒作用。 - 如果调用
notify()
的对象上有多个线程在等待,则随机唤醒一个。 - 为什么说
对象上有多个线程在等待
,出现这种情况,正如上面说的,调用notify()
需要先获取对象锁,而一个锁,可能就有多个线程在等待,所以这里才这么说。同时,也说明,调用了notify()
后,恢复的不一定就是前面对应调用了wait()
的线程,也可能是其他也在等待这同个对象锁的线程。 - 调用
notify()
后唤醒的线程执行完毕释放锁之后,如果还有其他还在等待的线程,那它们是不会接收到通知的,只会继续等待,直到另一个notify()
,或notifyAll()
被调用。
- 调用
notifyALL()
:线程唤醒。类似notify()
,都是唤醒该对象锁等待中的线程。区别是,notifyAll()
唤醒的是在此对象上等待的所有线程。yield()
:线程让步,即当前线程退出cpu的占用,回到就绪状态,重新排队等待cpu调用。
注:- 调用
yield()
后回到就绪状态,意味着随时可能再次被cpu调用到。所以就可能出现一退出cpu占用后,立马又被cpu调用回去的情况。 - 调用
yield()
,不会释放锁。
- 调用
举个例子,加深印象:
public class TestThreadWaitNotify {
//测试service类
public static class TestService{
//测试wait
public void testWait(Object lock) {
try {
synchronized (lock) {
System.out.println(new Date() + " | begin wait() ThreadName:"+Thread.currentThread().getName());
lock.wait();
System.out.println(new Date() + " | end wait() ThreadName:"+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//测试notify
public void testNotify(Object lock) {
try {
synchronized (lock) {
System.out.println(new Date() + " | begin notify() ThreadName:"+Thread.currentThread().getName());
lock.notify();
Thread.sleep(3000); // 休眠三秒,以展示需要执行完同步块内容,才真正执行notify唤醒的效果
System.out.println(new Date() + " | end notify() ThreadName:"+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//测试notifyAll
public void testNotifyAll(Object lock) {
try {
synchronized (lock) {
System.out.println(new Date() + " | begin notifyAll() ThreadName:"+Thread.currentThread().getName());
lock.notifyAll();
Thread.sleep(3000);// 休眠三秒,以展示需要执行完同步块内容,才真正执行notifyAll唤醒的效果
System.out.println(new Date() + " | end notifyAll() ThreadName:"+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//调用wait()的线程
public static class ThreadWait extends Thread{
private Object lock;
public ThreadWait (Object lock) {
super();
this.lock = lock;
}
public void run() {
TestService service = new TestService();
service.testWait(lock);
}
}
//调用notify()的线程
public static class ThreadNotify extends Thread{
private Object lock;
public ThreadNotify (Object lock) {
super();
this.lock = lock;
}
public void run() {
try {
sleep(3000); //休眠3秒,为了在wait线程全部调用完再调用到
TestService service = new TestService();
service.testNotify(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//调用notifyAll()的线程
public static class ThreadNotifyAll extends Thread{
private Object lock;
public ThreadNotifyAll (Object lock) {
super();
this.lock = lock;
}
public void run() {
try {
sleep(8000); //休眠8秒,使其最后才被调用到
TestService service = new TestService();
service.testNotifyAll(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//随便新建一个Object作为对象锁
Object lock = new Object();
//创建四个线程,轮流 获取资源->等待->释放资源
Thread wait1 = new ThreadWait(lock);
wait1.setName("wait-1");
wait1.start();
Thread wait2 = new ThreadWait(lock);
wait2.setName("wait-2");
wait2.start();
Thread wait3 = new ThreadWait(lock);
wait3.setName("wait-3");
wait3.start();
Thread wait4 = new ThreadWait(lock);
wait4.setName("wait-4");
wait4.start();
//唤醒一个等待中的线程
Thread notify1 = new ThreadNotify(lock);
notify1.setName("notify-1");
notify1.start();
//再唤醒一个等待中的线程
Thread notify2 = new ThreadNotify(lock);
notify2.setName("notify-2");
notify2.start();
//唤醒剩余两个等待中的线程
Thread notify3 = new ThreadNotifyAll(lock);
notify3.setName("notify-3");
notify3.start();
}
}
测试结果为:
Tue May 26 09:26:50 CST 2020 | begin wait() ThreadName:wait-1
Tue May 26 09:26:50 CST 2020 | begin wait() ThreadName:wait-2
Tue May 26 09:26:50 CST 2020 | begin wait() ThreadName:wait-3
Tue May 26 09:26:50 CST 2020 | begin wait() ThreadName:wait-4
Tue May 26 09:26:53 CST 2020 | begin notify() ThreadName:notify-1
Tue May 26 09:26:56 CST 2020 | end notify() ThreadName:notify-1
Tue May 26 09:26:56 CST 2020 | end wait() ThreadName:wait-1
Tue May 26 09:26:56 CST 2020 | begin notify() ThreadName:notify-2
Tue May 26 09:26:59 CST 2020 | end notify() ThreadName:notify-2
Tue May 26 09:26:59 CST 2020 | end wait() ThreadName:wait-2
Tue May 26 09:26:59 CST 2020 | begin notifyAll() ThreadName:notify-3
Tue May 26 09:27:02 CST 2020 | end notifyAll() ThreadName:notify-3
Tue May 26 09:27:02 CST 2020 | end wait() ThreadName:wait-4
Tue May 26 09:27:02 CST 2020 | end wait() ThreadName:wait-3
过程解析:
1、由于ThreadNotify、ThreadNotifyAll线程执行了sleep()方法,导致线程休眠,所以首先是4个ThreadWait线程抢占了cpu,先执行;
2、这4个ThreadWait线程,轮流 获取了锁,然后调用了wait()释放了锁,进入等待。
释放了的锁,又被下一个ThreadWait线程获取到,循环往复,直到最后一个ThreadWait线程(wait-4)调用wait()释放了锁,进入等待队列;
3、3秒后,所有ThreadNotify线程休眠结束,线程[notify-1] 获取到由线程[wait-4]释放的锁;
4、获取到锁的线程[notify-1]执行完synchronized同步块里面的代码(notify()后休眠了3秒),释放了锁;
5、由线程[notify-1]释放的锁随机唤醒了一个ThreadWait线程(wait-1),线程[wait-1]获取到锁,继续执行剩下的代码。
此时还有3个ThreadWait线程(wait-2、wait-3、wait-4)还在等待中;
6、上面获取到锁的线程[wait-1]执行完同步块代码,释放了锁。此时第二个ThreadNotify线程(notify-2)获取到锁,执行了notify()方法。
(因为此时其他三个ThreadWait线程还在等待,ThreadNotifyAll线程(notify-3)还在休眠,所以是ThreadNotify线程(notify-2)获取到锁);
7、由线程[notify-2]释放的锁,又随机唤醒了一个ThreadWait线程(wait-2),线程[wait-2]获取到锁,继续执行剩下的代码。
此时还有2个ThreadWait线程(wait-3、wait-4)还在等待中;
8、线程[wait-2]执行完同步代码块后释放了锁,锁被线程[notify-3]获取到,执行notifyAll()方法,释放了锁,唤醒所有其他等待中的线程;
9、剩下的两个等待中的ThreadWait线程(wait-3、wait-4),其中线程[wait-4]先获取到锁,执行完其同步代码块,释放锁;
10、最后一个ThreadWait线程(wait-3)获取到锁,执行完同步代码块,释放了锁;
11、所有线程执行完毕,程序结束
6、常用方法—线程中断
interrupt()
:线程中断。参考JDK中的说明,简单的理解就是:interrupt()
方法实际上只是通知线程,修改线程的中断状态标识为true
,并不会直接中止该线程。具体做什么事情由写代码的人决定(通常我们会结束该线程)。- 如果线程在阻塞状态时(调用了
wait()
、sleep()
、join()
等引起阻塞的方法),调用了它的interrupt()
方法,则其中断状态将被清除(即置为false
),还将收到一个InterruptedException
异常。 - 如果线程被阻塞在一个
Selector
选择器中,那么通过interrupt()
中断它时,线程的中断标记会被设置为true
,并且它会立即从选择操作中返回。 - 中断一个“已终止的线程”不会产生任何操作。
- 如果线程在阻塞状态时(调用了
isInterrupted()
:调用指定线程的isInterrupted()
方法,可以获取线程的中断状态标志。interrupted()
:调用静态方法Thread.interrupted()
,会先返回当前线程的中断状态,然后再清除当前线程的中断状态(即置为false
)。
综上所述,多线程的中断机制,其实就是修改中断标记
的机制,相关的有以下三个方法:
interrupt()
,设置当前中断标记为true
。isInterrupted()
,检测当前的中断标记。interrupted()
,检测当前的中断标记,然后重置中断标记为false
。
举个例子,加深印象:
/**
* 主类:
* 1、创建启动线程
* 2、让线程运行3秒钟
* 3、中断线程
**/
public class InterruptTest {
public static void main(String[] args) {
//1、创建、启动线程
Thread1 thread1 = new Thread1();
thread1.start();
try {
//2、挂起主线程3秒,执行thread1
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3、thread1执行中断
thread1.interrupt();
System.out.println("thread1 执行中断");
}
}
/**
* 线程类:
* 调用 isInterrupted() 判断中断状态
* 根据中断状态,自定义编码结束线程
**/
public static class Thread1 extends Thread{
public void run() {
while (true){
if(isInterrupted()){
System.out.println(new Date() + " | 当前线程 isInterrupted()为 " + isInterrupted() + " 结束当前线程");
break;
} else {
System.out.println(new Date() + " | 当前线程 isInterrupted()=为 " + isInterrupted() + " 继续执行当前线程");
}
}
}
}
测试结果为:
Tue May 26 10:31:34 CST 2020 | 当前线程 isInterrupted()为 false 继续执行当前线程
...
...
Tue May 26 10:31:35 CST 2020 | 当前线程 isInterrupted()为 false 继续执行当前线程
Tue May 26 10:31:35 CST 2020 | 当前线程 isInterrupted()为 true 结束当前线程
thread1 执行中断
过程解析:
1、创建、启动Thread1线程;
2、主线程调用sleep(1000)休眠了1秒,让出cpu,执行Thread1线程;
3、这1秒内,Thread1线程一直在判断其是否中断;
4、1秒后,随机轮到了主线程执行,主线程调用了thread1.interrupt() 中断thread1线程;
5、随机切换到了thread1线程执行,判断是否中断的时候,确认中断了,程序执行break,线程结束;
6、切换回主线程,调用完最后一句代码,程序结束。
7、常用方法—等待线程结束
场景:线程A的业务逻辑,有一部分需要在线程B执行完成后,才能继续执行。这时候,就需要我们在线程A执行过程中,在需要线程B执行的时候,进行阻塞,切换执行线程B,等待线程B执行完成后,再恢复就绪状态。
根据前面介绍的方法,可以通过wait()
、notify()
,来对线程A调用等待,线程B完成后调用唤醒。但现在介绍一个更简便的方式,就是使用join()
。
join()
:等待其他线程结束。在当前线程中调用其他线程t
的join()
方法(t.join()
)后,当前线程阻塞,等线程t
结束了,当前线程才恢复就绪状态。join()
内部是使用wait()
来实现的,所以join()
会释放锁。
join(long)
:long参数为设定指定的等待时间。- 当指定时间后,
join()
线程还未结束,则还是按指定时间,原来的线程结束阻塞,进入就绪状态,join()
线程状态不变。 - 在指定时间内,
join()
线程提前结束,则原来的线程提前进入就绪状态。
- 当指定时间后,
举个例子:
public class TestThreadJoin {
public static class Thread1 extends Thread{
public void run() {
try {
sleep(1000); // 休眠1秒,意在让出cpu,让主线程先执行join
System.out.println(new Date() + " | join Thread1 ");
sleep(2000);
System.out.println(new Date() + " | end Thread1 ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
System.out.println(new Date() + " | main Thread start ");
Thread thread1 = new Thread1();
thread1.start();
thread1.join();
System.out.println(new Date() + " | main Thread end ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试结果:
Tue May 26 17:59:50 CST 2020 | main Thread start
Tue May 26 17:59:51 CST 2020 | join Thread1
Tue May 26 17:59:53 CST 2020 | end Thread1
Tue May 26 17:59:53 CST 2020 | main Thread end
执行解析:
1、主线程创建、启动Thread1线程;
2、Thread1线程 run() 里调用 sleep() ,意在让出cpu,由主线程先执行 thread1.join() 方法;
3、主线程执行 thread1.join() 方法,cpu切换到thread1线程;
4、thread1线程执行完毕;
5、cpu切换回主线程,执行余下代码;
6、程序结束。
注:
如果thread1线程不一开始就调用sleep()休眠、让出cpu,则可能在主线程调用join()之前,thread1线程就已经执行,甚至执行完成了。
此时程序不会报错,逻辑还是正确的。
因为调用thread1.join()后,系统检测到thread1正在执行,就只是省去了将thread1切换到运行状态而已。
如果调用join()时,thread1已经执行结束,则马上返回到主线程。
如果调用join()时,thread1还未结束,则按照原本的逻辑继续执行。
8、常用方法—获取、设置
currentThread()
:获取当前运行中的线程,Thread
类静态方法,返回当前线程的Thread
对象。isAlive()
:判断线程是否处于活动状态,返回为boolean
。activeCount()
:返回当前活跃的线程数量,Thread
类静态方法,返回为int
。- 当前活跃的线程,包括了休眠中、等待中的所有线程。
getId()
: 获取线程唯一标识,返回long
。getState()
:获取线程的状态,返回Thread.State
(线程状态枚举类)。setName(String)
:设置线程名。getName()
:获取线程名,返回String
。setPriority(int)
:设置线程优先级。getPriority()
:获取线程优先级,返回int
。setDaemon(boolean)
:设置线程为守护线程。isDaemon()
:判断线程是否为守护线程,返回boolean
。
9、线程优先级
- 线程可以划分优先级,优先级更高的线程可以获得更多的cpu资源,即cpu优先执行优先级更高的线程。
- 优先级具有随机性。优先级更高的线程优先执行,是包含着随机性的,即不一定完全按照优先级执行完线程。因为优先级更高,也只是获得的cpu时间片更多而已。本质上,还是需要cpu切换时间片执行的。
- 优先级等级分为1-10,其中10的优先级最高。
调用方法setPriority()
可以设置。
如果设置范围不在1-10内,会报错。 - 优先级具有继承性。被启动的线程,拥有与启动它的线程一样的优先级。譬如:线程A启动线程B,则线程B拥有与线程A一样的优先级。
- 优先级等级差距越大,效果越明显。
因为等级差距越大,获得的cpu资源差距就越大,cpu调用的频率就差得越多,优先级高的执行的机会就越多,低的执行的机会就越少。
10、守护线程
- 多线程可以分为两种:用户线程、守护线程。
- 一般我们创建的都是用户线程。想要创建守护线程,可以在线程
start()
前,调用setDaemon(true)
方法将其设置为守护线程。 - 守护线程,顾名思义,可以看作是守护其他线程的线程。也因此,当其他线程都结束后,守护线程没有了守护对象,就会自行结束。
所以,守护线程基本与其他非守护线程一样,只有一点不同,就是当系统中不存在非守护线程的时候,守护线程会自动销毁。 - 典型的例子就是垃圾回收线程,当程序中有其他线程运行的时候,就会产生垃圾,垃圾回收线程就会一直存在。当程序中没有其他线程在运行了,也就不会产生垃圾,垃圾回收线程也就无垃圾可回收,就会自动结束了。
- 守护线程的优先级较低。
- 守护线程的子线程,也是守护线程。(类似于优先级的继承性)
11、线程的状态
根据线程不同的运行情况,会有不同的状态,这些状态存在于Thread.State
枚举类中,分别为:
- NEW:创建完,尚未启动的线程,处于这种状态。
- RUNNABLE:正在JVM中执行的线程,处于这种状态。
- BLOCKED:受阻塞并等待锁的线程,处于这种状态。
- WAITING:无限期地等待另一个线程来执行某一特定操作的线程,处于这种状态。
- TIMED WAITING:等待另一个线程来执行取决于指定等待时间的操作的线程,处于这种状态。(相比WAITING状态,这里指有指定的等待时间)
- TERMINATED:已退出的线程,处于这种状态。
线程在实际运行过程中,线程状态的改变主要由JVM的调度和各个线程方法的调用引起,线程的状态切换过程具体如下图:
12、线程本地变量
多线程要共享一个变量,可以通过public static
来定义变量,这样在每个线程中都可以使用到这个变量(需要特别注意线程安全)。
但如果每个线程要共享一个变量,又要保持各自对该变量的修改,public static
就满足不了了,因为它一旦被改动,所以用到它的地方都会随之改变。所以此时就需要用到线程的本地变量了。
ThreadLocal
:线程本地变量,它可以隔离每个线程的共享变量。即复制变量到每个线程里,每个线程里调用的都是复制后的独立的副本,不会被其他线程影响。get()
:获取当前值set()
:设置当前值remove()
:移除当前值修改默认值
:线程本地变量默认值是null
。如果要修改默认值,使得每个使用到这个变量的线程能读取到这个默认值,就需要创建一个类,来继承ThreadLocal
,并重写其initialValue()
方法,在return
上返回默认值。
如:
//ThreadLocal中initialVal方法源码
protected T initialValue() {
return null;
}
//我们可以新建一个类来继承,并重写initialVal方法,修改默认值
public class ThreadLocalMyVal extends ThreadLocal{
protected Object initialValue() {
return "新的默认值";
}
}
InheritableThreadLocal
:父线程传递到子线程的本地变量,是ThreadLocal
的子类。如:在主线程创建并启动子线程A,此时主线程调用InheritableThreadLocal
进行赋值,子线程A可以通过InheritableThreadLocal
获取到父线程赋的值,二者调用的是同一个InheritableThreadLocal
。即只对非父子关系的线程进行隔离。
举个例子,加深印象:
//定义共享变量
public class ThreadLocalVal {
public static ThreadLocal tl = new ThreadLocal();
public static String s;
}
//创建3个线程,各自在内部调用共享变量,修改值,并打印
public class TestThreadLocalTest {
public static class Thread1 extends Thread{
public void run() {
//修改ThreadLocal值,并打印
try {
for(int i = 0; i < 5; i++) {
System.out.println("ThreadName:" + this.getName() + " --"+ThreadLocalVal.tl.get()+"-ThreadLocal");
sleep(100);
ThreadLocalVal.tl.set(this.getName()+"-"+i);
System.out.println("ThreadName:" + this.getName() + " --"+ThreadLocalVal.s+"-String");
sleep(100);
ThreadLocalVal.s = this.getName()+"-"+i;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//创建三个线程,相互抢占cpu,看共享变量情况
Thread thread1 = new Thread1();
thread1.setName("t1");
thread1.start();
Thread thread2 = new Thread1();
thread2.setName("t2");
thread2.start();
Thread thread3 = new Thread1();
thread3.setName("t3");
thread3.start();
}
}
测试结果:
ThreadName:t3 --null-ThreadLocal
ThreadName:t2 --null-ThreadLocal
ThreadName:t1 --null-ThreadLocal
ThreadName:t2 --null-String
ThreadName:t3 --null-String
ThreadName:t1 --null-String
ThreadName:t3 --t3-0-ThreadLocal
ThreadName:t2 --t2-0-ThreadLocal
ThreadName:t1 --t1-0-ThreadLocal
ThreadName:t3 --t1-0-String
ThreadName:t2 --t1-0-String
ThreadName:t1 --t1-0-String
ThreadName:t3 --t3-1-ThreadLocal
ThreadName:t2 --t2-1-ThreadLocal
ThreadName:t1 --t1-1-ThreadLocal
ThreadName:t2 --t1-1-String
ThreadName:t3 --t1-1-String
ThreadName:t1 --t1-1-String
ThreadName:t3 --t3-2-ThreadLocal
ThreadName:t2 --t2-2-ThreadLocal
ThreadName:t1 --t1-2-ThreadLocal
ThreadName:t3 --t1-2-String
ThreadName:t2 --t1-2-String
ThreadName:t1 --t1-2-String
ThreadName:t2 --t2-3-ThreadLocal
ThreadName:t3 --t3-3-ThreadLocal
ThreadName:t1 --t1-3-ThreadLocal
ThreadName:t3 --t1-3-String
ThreadName:t2 --t1-3-String
ThreadName:t1 --t1-3-String
执行解析:
1、分别开启了3个线程,这时3个线程会抢占cpu,交错执行;
2、每个线程都不断地修改两个共享变量的值;
3、一开始日志的输入都是null,证明ThreadLocal默认值为null;
4、仔细观察日志可以发现,普通共享变量(Sting),3个线程每次打印日志的时候,都是返回一样的值。证明这时3个线程调用的都是同一个变量;
5、线程的本地变量(ThreadLocal),3个线程每次调用的,都是返回与各自线程名对应的值。证明每次线程调用的都是各自的本地变量。
13、并发集合
多线程中,使用共享数据,会引发同步问题。所以就必须加锁。
除了手动加锁,java中还有一些集合类,已经帮我们做好了相应的锁逻辑。
常用的集合中,HashMap
非线程安全,HashTable
是线程安全,但是HashTable
每次是锁住整张表来让线程独占,效率低。
而ConcurrentHashMap
则不仅是线程安全的,同时还提高了效率。
ConcurrentHashMap
的实现原理:
ConcurrentHashMap
内部由Segment
(段)组成,默认有16个,每个Segment
的数据结构类似于HashMap
。
对ConcurrentHashMap
加入新的内容或者修改,不需要对整个ConcurrentHashMap
加锁,而只需根据内容的hashCode,计算出对应的Segment
,对该Segment
加锁即可。
这样,在多线程操作的时候,每个线程都需要先获取Segment
的锁,就可以保证同步的安全性。
同时,要是每个线程对应的Segment
不是同一个,又可以实现真正的并发,就算有些是同个的,也大大降低了多个线程等待同个锁的概率,提高了效率。
14、线程内异常的传递
多线程中的产生异常,可以使用UncaughtExceptionHandler
类进行捕获。
setUncaughtExceptionHandler()
:设置指定线程的异常处理器。setDefaultUncaughtExceptionHandler()
:设置所有线程默认的异常处理器。
设置异常处理器的时候,需要新建类,实现UncaughtExceptionHandler
异常处理器接口,实现接口的uncaughtException
方法。
举个例子,加深印象:
//让线程报错,调用两种方式来捕获
public class TestThreadError {
public static class Thread1 extends Thread{
public void run() {
//报空指针异常
String a = null;
System.out.println(a.toString());
}
}
public static void main(String[] args) {
//设置默认的异常捕获器
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " -- Default");
e.printStackTrace();
}
});
Thread1 thread1 = new Thread1();
thread1.setName("t1");
//设置线程t1自身的异常捕获器
thread1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " -- current");
e.printStackTrace();
}
});
thread1.start();
//不设置捕获器,使用默认的
Thread1 thread2 = new Thread1();
thread2.setName("t2");
thread2.start();
}
}
测试结果:
t1 -- current
t2 -- Default
java.lang.NullPointerException
at testApi.TestThreadError$Thread1.run(TestThreadError.java:9)
java.lang.NullPointerException
at testApi.TestThreadError$Thread1.run(TestThreadError.java:9)
执行解析:
1、先设置了默认异常处理器,然后创建线程[t1]的时候,再给[t1]设置自己的异常处理器,线程[t2]不设置自己的异常处理器;
2、线程[t1]执行,报错,调用了自身的异常处理器捕获了异常;
3、线程[t2]执行,报错,调用了默认的异常处理器捕获了异常;
4、通过以上测试也可以知道,默认先调用线程自身的异常处理器,没有了才调用公共的。
注:使用单例的SimpleDateFormat
在多线程中极易出现日期转换出错,所以平时使用需谨慎虑多线程情况。
(解决方法可以是各个线程各自创建SimpleDateFormat
实例,也可以使用ThreadLocal
绑定SimpleDateFormat
)