一、线程的创建
1.1、继承Thread
// 自定义类继承 Thread
public class Thread01 extends Thread{
@Override
public void run() {
System.out.println("Hello World");
}
}
public static void main(String[] args) throws Exception {
Thread01 thread01 = new Thread01();
// 启动线程
thread01.start();
}
Thread常用方法:
public synchronized void start() : 启动线程
public void run():线程具体执行的方法,需要在继承的类中重写
public static native void sleep(long millis) :线程休眠,需要传入休眠的时间,该方法会暂停线程执行直到线程休眠时间结束
public static void sleep(long millis, int nanos):跟上一个方法一样,只不过多传了个时间机制
public void interrupt():给线程打上一个中断标识
public boolean isInterrupted():判断线程是否被打上中断标识
public final synchronized void setName(String name):给线程设置一个名称
public final void join():让主线程等待子线程执行结束
public static native void yield():表示当前线程愿意让出CPU使用权
1.2、实现Runnable接口
因为 Runnable 没有给我们提供启动线程的方法,因此我们需要借助 Thread 类来启动。
// 自定义类实现 Runnable 接口
public class Thread02 implements Runnable {
@Override
public void run() {
System.out.println("Hello World");
}
}
public static void main(String[] args) throws Exception {
Thread02 thread02 = new Thread02();
// 借助 Thread 类来启动
Thread thread = new Thread(thread02);
// 启动线程
thread.start();
}
1.3、实现Callable接口
上面的两种方式创建线程有一个缺点,但是无法获取到线程执行的结果,我们可以通过实现 Callable 接口从而获得线程执行的结果。
Callable 也没有提供启动线程的方法,因此我们也需要借助 Thread 类来启动。
但是我们可以看到 Thread 除了 Runnable 传参的构造方法之外,并没有提供 Callable 传参的构造方法,因此我们无法直接通过 Thread 来启动,需要一个媒介,这个媒介就是 FutureTask。
// 自定义 Callable 实现
public class Thread03 implements Callable<String> {
@Override
public String call() throws Exception {
return "这是一个线程执行的结果.....";
}
}
public static void main(String[] args) throws Exception {
Thread03 thread03 = new Thread03();
// 包装成 FutureTask
FutureTask futureTask = new FutureTask(thread03);
// 借助 Thread 类启动
Thread thread = new Thread(futureTask);
thread.start();
// 获取执行的结果
System.out.println(futureTask.get());
}
1.4、Executor线程池
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
二、线程的生命周期
- New:新建态: new Thread ~ thread.start期间。
- Runnable:可执行态: 可被CPU调度执行期间。
- Running:运行态: 线程获取CPU权限进行执行。
- Blocked:阻塞状态: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。
- 等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用)时。
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时。
- Dead:死亡状态:线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
三、线程的中断策略
我们有时候可能会终止线程的运行,比如某些中间件,在正常关闭的时候可能需要终止某些正在运行的线程。
Thread 提供了一个 stop() 方法,这个方法会直接终止线程,不给任何喘息的机会。
但是这么暴力的终止线程也会存在一个问题,就是如果线程正在执行某些任务,恰好这个线程持有 ReentrantLock 锁 ,一旦我们通过暴力的形式终止线程,线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那么这个锁将永远得不到释放,因此 JDK 官方是不建议使用 stop 方法来终止线程,另外这个方法也被标注为过时了。
Thread 给我们提供了一个interrupt()
方法, 这个方法相对来说就优雅的多了。
当调用这个方法的之后,并不会终止线程,而是会给这个线程打上一个终止标记,标记这是个终止的线程,后续处理的时候,我们只需要调用 isInterrupted() 方法判断该线程是否是终止线程,如果是的话我们再做终止处理,同时我们也可以无视这个终止标记。
除了调用 isInterrupted() 主动检测外,另外一种就是通过异常来检测,
如果其他线程调用了该线程的interrupt()
方法,当调用wait()、join()、sleep()
方法时都会抛出一个 InterruptedException 异常。
四、线程之间的通信
线程与线程之间不完全是互相独立的,也需要某种通信机制来达到互相协助的目的。
4.1、sleep()
-
单独使用
暂停当前线程,进入Blocked状态,把cpu片段让出给其他线程。
public class Main0 { static SimpleDateFormat sdf = new SimpleDateFormat("mm:ss"); public static void main(String[] args) { new Thread(new Car()).start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new Ambulance()).start(); } private static class Car implements Runnable { @Override public void run() { System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑"); System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点"); } } private static class Ambulance implements Runnable { @Override public void run() { System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车开始启动,在路上跑"); System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车跑到终点"); } } }
---->[运行结果]----------------------
02:29:小汽车开始启动,在路上跑
02:29:小汽车跑到终点
02:31:救护车开始启动,在路上跑
02:31:救护车跑到终点
修改一下,在子线程内sleep
private static class Car implements Runnable {
@Override
public void run() {
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点");
}
}
18:48:小汽车开始启动,在路上跑
18:50:救护车开始启动,在路上跑
18:50:救护车跑到终点
18:53:小汽车跑到终点
-
搭配锁使用
在线程中加synchronized(
这里锁用sdf对象
)。暂停当前线程,进入Blocked状态,把cpu片段让出给其他线程。并且不会释放锁。
public class Main2 { static SimpleDateFormat sdf = new SimpleDateFormat("mm:ss"); public static void main(String[] args) { new Thread(new Car()).start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new Ambulance()).start(); } private static class Car implements Runnable { @Override public void run() { synchronized (sdf){ System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点"); } } } private static class Ambulance implements Runnable { @Override public void run() { synchronized (sdf){ System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车开始启动,在路上跑"); System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车跑到终点"); } } } }
Car线程和Ambulance线程在运行时使用同一把锁,线程在休眠时不会释放锁,所以Ambulance线程需要等待Car线程执行完成,才能进行执行。
23:46:小汽车开始启动,在路上跑
23:51:小汽车跑到终点
23:51:救护车开始启动,在路上跑
23:51:救护车跑到终点
问题:Thread.sleep() 会导致ANR吗?
首先,先明白一个问题:什么是 ANR?
Application Not Responding,意思是” 应用没有响应 “
以前我的理解就是 “在主线程做了耗时操作” 就会引起 ANR,现在我觉得我是错误的,ANR 的意思是应用没有响应,耗时操作实际上并不一定会导致没有响应,我对没有响应的理解是:
有人(事件或操作)发出了一个请求,但是主线程没有对这个人进行反馈(可能是没时间、可能是不想理、可能是手被绑住了没有办法理你), 这个叫没有响应
那举个例子:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // do some blablabla... Log.d("test", "准备sleep30秒") Thread.sleep(30000) Log.d("test", "sleep30秒完成") // do some blablabla... }
这段代码在 onCreate 中 sleep 了 30 秒,会出现 ANR 吗?
答案是:可能会,也可能不会。
当主线程在 Sleep 的时候,如果 UI 线程不需要进行操作,也就是说没有消息会发送给 UI 线程并要求 UI 线程进行处理的时候,Sleep 30 秒就不会导致 ANR,因为没有出现 ANR(应用没有响应)的情况,没有人向线程请求什么东西,也就不需要响应了,既然没有响应了,那怎么会有 ANR 呢?
但是,线程在 Sleep 的时候,主线程有接收到需要处理的请求的时候,
需要注意的是,需要处理的请求,不一定只是用户的手动触摸,也有可能是其他线程需要对线程进行 UI 更新的请求,这个时候 UI 线程正在 Sleep,根本没有办法理你(不想理你),这就符合了ANR的条件,所以会出现 ANR(比如说在这 30 秒内,点击了 返回按钮,就会出现 ANR)。
4.2、join()
阻塞当前线程,直到join的线程结束或超时。
普通代码:
public class Main6 {
static SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
Thread car = new Thread(new Car());
car.start();
System.out.println(sdf.format(System.currentTimeMillis()) + ":main线程结束");
}
private static class Car implements Runnable {
@Override
public void run() {
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑");
try {
Thread.sleep(3000);//模拟执行3s的任务之后
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点");
}
}
}
26:28:小汽车开始启动,在路上跑
26:28:main线程结束
26:31:小汽车跑到终点
加入join:
public class Main6 {
static SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
Thread car = new Thread(new Car());
car.start();
try {
car.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(System.currentTimeMillis()) + ":main线程结束");
}
private static class Car implements Runnable {
@Override
public void run() {
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑");
try {
Thread.sleep(3000);//模拟执行3s的任务之后
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点");
}
}
}
---->[打印结果]-----------------------------
31:53:小汽车开始启动,在路上跑
31:56:小汽车跑到终点
31:56:main线程结束
4.3、yield()
让出线程的cpu调度权,之后再一起抢夺。
运行状态-->就绪状态
。
public class Main7 {
public static void main(String[] args) {
Thread car = new Car("car");
Thread ambulance = new Car("Ambulance");
car.start();
ambulance.start();
}
private static class Car extends Thread {
public Car(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "----" + i);
if (i == 5) {
yield();
}
}
}
}
}
运行了几组数据,大部分满足让出调度权后,另一个线程执行,也有少量情况不是。所以yield说明我现在不急,可以划划水,执行权可以让出去。不过接下来谁执行还是不一定的。
4.4、wait()、notifyAll()、notify()
Object 给我们提供了 wait()、notifyAll()、notify()
三个方法,线程调用 wait() 方法会进入到等待,调用 notifyAll()、notify() 会唤醒等待的线程。
根据线程的生命周期图可知:
在
线程t1
调用A对象
的wait()方法
,会释放t1持有的锁
,让线程t1
进入等待队列(Blocked状态)
直到其他线程调用A对象
的notify()方法
或notifyAll()
方法线程t1
进入同步队列(Blocked状态)
当线程t1
获得锁后会进入就绪状态Runnable
,获取CPU的调度权后会继续执行。
既然是释放当前线程的锁,那么必须有锁才行,而且必须用该锁的对象调用wait方法,
比如下面是用sdf对象加锁的,必须使用sdf.wait()
,否则会抛出InterruptedException
。
private static class Car implements Runnable {
@Override
public void run() {
synchronized (sdf) {
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动,在路上跑");
try {
Thread.sleep(3000);//模拟执行3s的任务之后
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车紧急刹车....");
sdf.wait();
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车开始启动....");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(System.currentTimeMillis()) + ":小汽车跑到终点");
}
}
}
private static class Ambulance implements Runnable {
@Override
public void run() {
synchronized (sdf) {
System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车开始启动,在路上跑");
System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车跑到终点");
}
}
}
在Car线程调用sdf.wait()
后,锁将被释放,然后Ambulance线程就可以持有锁运行了,如果不唤醒线程,线程将一直阻塞,就是根本停不下来
。
打个比方就是sdf是司机,
wait()
之后就把车钥匙扔了,然后熄火了。钥匙拿不回来,车就跑不了。需要notify()
获取车钥匙。
30:48:小汽车开始启动,在路上跑
30:51:小汽车紧急刹车....
30:51:救护车开始启动,在路上跑
30:51:救护车跑到终点
然后就阻塞在这里停不下来了....
使用notify
唤醒等待线程:
注意wait和notify需要使用同一个对象,否则然并卵。 在Ambulance线程跑完后唤醒Car线程,然后Car获取锁后会进入就绪态。
private static class Ambulance implements Runnable {
@Override
public void run() {
synchronized (sdf) {
System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车开始启动,在路上跑");
System.out.println(sdf.format(System.currentTimeMillis()) + ":救护车跑到终点");
sdf.notify();
}
}
}
40:23:小汽车开始启动,在路上跑
40:26:小汽车紧急刹车....
40:26:救护车开始启动,在路上跑
40:26:救护车跑到终点
40:26:救护车:喂,哥们,醒醒,你可以开了...
40:26:小汽车开始启动....
40:31:小汽车跑到终点
五、ThreadLocal
5.1、主要作用
ThreadLocal的作用主要是:做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。
5.2、使用场景
-
Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
-
我在项目中存在一个线程,经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
before void work(User user) { getInfo(user); checkInfo(user); setSomeThing(user); log(user); } then void work(User user) { try{ threadLocalUser.set(user); // 他们内部 User u = threadLocalUser.get(); 就好了 getInfo(); checkInfo(); setSomeThing(); log(); } finally { threadLocalUser.remove(); } }
-
在java后台中,很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
5.3、底层实现原理
我先说一下他的使用:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
- 每个Thread里都有一个名叫:threadLocals的成员变量,数据类型为ThreadLocalMap,初始为null。
- ThreadLocalMap里有个Entry数组,含有两个属性:key数据类型为ThreadLocal,value数据类型为Object(value就是真正的数据所在)。
- ThreadLocal调用set方法时,获取当前Thread里的ThreadLocalMap,进行判空和初始化,然后将自身作为key,数据作为value,存储进Entry数组。
- ThreadLocal调用get方法时,获取当前Thread里的ThreadLocalMap,将自身作为key,从Entry数组里获取数据。从而实现了数据隔离。
5.4、ThreadLocalMap底层结构的Entry为什么要设计为数组?
我们开发过程中,一个线程可以有多个ThreadLocal来存放不同类型的对象的,但是他们都将放到当前线程的ThreadLocalMap里,所以肯定要数组来存。
5.5、ThreadLocal既然是线程隔离的,对象是存储在Java栈的吗?
从前面我们知道,Java中堆是线程共享的,栈是线程私有的,那么ThreadLocal对象是在栈上吗?
答案是否定的,ThreadLocal对象是在堆上,其变量名和地址值在栈上,该地址值指向引用的对象。
原因:ThreadLocal是被其创建的类持有,根源上其实例是被线程持有,所以它的值也是被线程持有,它们都在堆上,只是通过一些技巧将可见性修改成了线程可见。什么技巧呢?就是上述底层实现原理。
5.6、ThreadLocal的弊端
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC会被回收,而value被留在了堆中,而我们没办法通过引用访问它,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
那怎么解决?
只要记得在使用的最后用remove把值清空就好了。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
5.7、为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和 Entry 中value一样内存泄漏的场景。
5.8、如果我想共享线程的ThreadLocal数据怎么办?
使用 InheritableThreadLocal 可以实现多个线程访问ThreadLocal的值。