一、认识多线程
1、Java程序天生就是多线程的
程序代码:
package top.jacktgq.ch1;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
/**
*
* @author 糖果墙
* @createDate 2019年7月26日 下午8:45:15
* @description 打印程序执行的所有线程信息
*
*/
public class OnlyMain {
public static void main(String[] args) {
//虚拟机管理的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + ":" + threadInfo.getThreadName());
}
}
}
运行结果:
Reference Handler:用于清除引用的线程
Finalizer:调用对象的Finalizer方法的线程,守护线程
Signal Dispatcher:发送虚拟机信号的线程
Attach Listener:可以获取当前程序运行相关的各种信息:如线程的映像,线程的栈,类信息的统计获取系统属性等的线程
由于该程序运行时间非常短,虚拟机判断该程序不需要GC(Gabarge collection),所以没有打印GC线程。
2、Java有几种新启线程的方式?——3种
程序代码:
package top.jacktgq.ch1;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
*
* @author 糖果墙
* @createDate 2019年7月26日 下午9:08:13
* @description 启动线程的三种方法
*
*/
public class NewThread {
/**
* 扩展自Thread类
*/
private static class UseThread extends Thread {
@Override
public void run() {
System.out.println("直接继承Thread类启动线程");
}
}
/**
* 实现Runnable接口
*/
private static class UseRun implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口启动线程");
}
}
/**
* 实现Callable接口,允许有返回值
*/
public static class UseCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("实现Callable接口启动线程");
return "CallResult";
}
}
/*public static void main(String[] args) throws Exception {
UseRun useRun = new UseRun();
useRun.run();
UseCall useCall = new UseCall();
useCall.call();
}*/
public static void main(String[] args) throws Exception {
//Thread
new UseThread().start();
//Runnable
UseRun useRun = new UseRun();
new Thread(useRun).start();
//Callable
UseCall useCall = new UseCall();
FutureTask<String> futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
运行结果:
异同点说明:实现Runnable接口和实现Callable接口两者很相似,细微的区别在于,实现Callable接口允许有返回值,且是线程阻塞的。
注:如果不把实现了Runnable或者继承了Thread的类的对象调用start()方法执行,那么这个类这就跟调用普通类的方法一样了。只有调用了start()方法了,java内部才回去调用系统的方法把这个类和操作系统真正的线程进行关联。
3、怎么样才能让java里的线程安全停止工作呢?
(1)自然执行完或抛出异常
(2)stop(),resume(),suspend(),pause()
注:stop()方法使用与早期的jdk版本,如今已经过时了,由于强制停止线程,故无法保证线程资源正常释放。suspend()方法也已过时,调用后线程被挂起的同时也不会释放资源,容易引发死锁问题。应该使用如下方法替代:
interrupt():中断一个线程并不是强行关闭这个线程,打个招呼,中断标志位置位true
isInterrupted():判定当前线程是否处与中断状态,判断这个中断标志位是否为true
static方法interrupted():同isInterrupted,但是会将中断标志位改为false
方法会抛出InterruptedException,线程的中断标志位会被复位成false,需要我们自己在catch里再次中断。
现在java线程是协作式,而不是抢占式的,当初设计的目的让每个线程都有自己的时间去做清理工作。
程序代码1:如何安全地中断继承Thread类启动的线程
package top.jacktgq.ch1.safeend;
/**
*
* @author 糖果墙
* @createDate 2019年7月26日 下午11:58:34
* @description 如何安全地中断继承Thread类启动的线程
*
*/
public class EndThreadTest {
private static int i = 0;
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
//获取当前运行的线程名
String threadName = Thread.currentThread().getName();
while(!isInterrupted()) {
i ++;
System.out.println(threadName + "is running " + i + " times!");
}
System.out.println(threadName + "interrupt flag is" + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
}
程序代码二:如何安全地中断实现Runnable接口启动的线程
package top.jacktgq.ch1.safeend;
/**
*
* @author 糖果墙
* @createDate 2019年7月27日 上午12:25:02
* @description 如何安全地中断实现Runnable接口启动的线程
*
*/
public class EndRunnableTest {
public static class UseRun implements Runnable {
@Override
public void run() {
//获取当前线程的名称
String threadName = Thread.currentThread().getName();
while(!Thread.currentThread().isInterrupted()) {
System.out.println(threadName + " is running!");
}
System.out.println(threadName + " interrupt flag is " + Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread useRun = new Thread(new UseRun());
useRun.start();
Thread.sleep(20);
useRun.interrupt();
}
}
程序代码三:如果线程发生了InterruptException异常了会发生什么情况
package top.jacktgq.ch1.safeend;
/**
*
* @author 糖果墙
* @createDate 2019年7月27日 上午12:25:26
* @description 如果线程发生了InterruptException异常了会发生什么情况
*
*/
public class HasInterruptException {
public static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while(!interrupted()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(threadName + " interrupt flag is " + isInterrupted());
e.printStackTrace();
}
System.out.println(threadName);
}
System.out.println(threadName + " interrupt flag is " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("UseThreadHasInterruptException");
endThread.start();
Thread.sleep(10000);
endThread.interrupt();
}
}
运行截图:
运行结果说明:
首先在主线程中启动子线程,子线程run方法中的while循环每循环一次,该线程都会睡5秒,主线程睡了10秒后会执行中断子线程的操作,具体过程如下:
(1)子线程的第一次循环正常打印了线程的名称;
(2)子线程的第二次循环睡了5秒,加上第一次一共睡了十秒,到了第10秒刚睡醒的时候,在主线程执行了中断子线程的操作,子线程的线程中断标志位被置为true,由于子线程还没切换到启动的状态,故而报了InterruptedException异常,然而在异常捕获的catch块中打印的线程中断标志位的值为false,然后继续向下执行打印了线程名,后面线程并没有因此停止,持续执行循环。
注意:按照之前安全中断线程的方法并不能中断线程继续执行,原因是方法会抛出InterruptedException,线程的中断标志位会被复位成false,需要我们自己在catch里再次中断,如下图所示。
4、线程的状态:
5、线程创建后调用start()方法和run()方法的区别:
直接调用run()方法程序代码:
package top.jacktgq.ch1;
/**
*
* @author 糖果墙
* @createDate 2019年7月29日 下午3:42:42
* @description 线程创建后调用start()方法和run()方法的区别
*
*/
public class StartAndRun {
private static class RunThread extends Thread {
@Override
public void run() {
int i = 90;
while(i > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am " + Thread.currentThread().getName() + " and now the i = " + i--);
}
}
}
public static void main(String[] args) {
Thread startAndRun = new RunThread();
startAndRun.setName("StartAndRun");
startAndRun.run();
//startAndRun.start();
}
}
运行截图:
调用start()方法程序代码:
package top.jacktgq.ch1;
/**
*
* @author 糖果墙
* @createDate 2019年7月29日 下午3:42:42
* @description 线程的创建后调用start()方法和run()方法的区别
*
*/
public class StartAndRun {
private static class RunThread extends Thread {
@Override
public void run() {
int i = 90;
while(i > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am " + Thread.currentThread().getName() + " and now the i = " + i--);
}
}
}
public static void main(String[] args) {
Thread startAndRun = new RunThread();
startAndRun.setName("StartAndRun");
//startAndRun.run();
startAndRun.start();
}
}
运行截图:
区别:如果新建线程后直接调用线程的run()的方法还是在主线程上运行,相当于调用了一个内部类中的普通方法,并没有发挥线程类的特性,而调用start()方法则会将线程类与一个操作系统线程建立对应关系,从而能真正开启一个新线程运行。
6、线程的优先级:
线程的优先级分为1~10,缺省为5,通过调用setPriority(int newPriority)进行设置。
注意:在某些操作系统运行环境中设置不一定生效。
7、守护线程
和主线程共死(主线程关闭,守护线程也会关闭),调用setDaemon(true|false)方法实现。finally不能保证一定执行,如果想保证finally语句一定会执行,则不能使用守护线程。
程序代码:
package top.jacktgq.ch1;
import top.jacktgq.ch1.safeend.HasInterruptException.UseThread;
/**
*
* @author 糖果墙
* @createDate 2019年7月29日 下午5:10:21
* @description 守护线程
*
*/
public class DaemonThread {
private static class UseThread extends Thread {
@Override
public void run() {
try {
String threadName = Thread.currentThread().getName();
while (!interrupted()) {
System.out.println(threadName + " is running!");
}
System.out.println(threadName + " interrupt flag is " + isInterrupted());
} finally {
System.out.println("......finally");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread daemonThread = new UseThread();
daemonThread.setDaemon(true);
daemonThread.start();
Thread.sleep(5);
//daemonThread.interrupt();
}
}
运行截图1:
运行截图2:
8、线程间的共享和同步
类锁:每个类的Class对象
8.1 原子性
(1)原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;
(2)原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
8.2 非原子性
也就是整个过程中会出现线程调度器中断操作的现象,例如:
类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:
(1)取出a和b
(2)计算a+b
(3)将计算结果写入内存
如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。
类似的,像"a++"这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。
8.3 可见性
可见性volatile修饰词,可以应对多线程同时访问修改同一变量,由于相互的不可见性所带来的不可预期的结果,存在二义性的现象,出现的。
多线程变量不可见:当一个线程对一变量a修改后,还没有来得及将修改后的a值回写到主存,而被线程调度器中断操作(或收回时间片),然后让另一线程进行对a变量的访问修改,这时候,后来的线程并不知道a值已经修改过,它使用的仍旧是修改之前的a值,这样修改后的a值就被另一线程覆盖掉了。
多线程变量可见:被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。
volatile使用场景:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在 synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
注意:
如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对long和double的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!
8.4 synchronized
synchronized为一段操作或内存进行加锁,它具有互斥性。当线程要操作被synchronized修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。
synchronized关键字和volatile关键字的区别:
a.synchronized关键字用于修饰方法或者单独构造代码块,volatile关键字用于修饰变量;
b.synchronized关键字既能确保可见性,又能确保原子性;而volatile只能确保可见性。
1)线程同步示例代码:
package top.jacktgq.ch1.syn;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: SynClzAndInst.java
* @Package top.jacktgq.ch1.syn
* @Description: 演示对象锁和类锁
* @author CandyWall
* @date 2019年10月8日 下午9:46:10
* @version V1.0
*/
public class SynClzAndInst {
private static class SynClass extends Thread {
@Override
public void run() {
System.out.println("TestClass is running...");
synClass();
}
}
private static class Instance1Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance1Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance1 is running..." + synClzAndInst);
synClzAndInst.instance1();
}
}
private static class Instance2Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..." + synClzAndInst);
synClzAndInst.instance2();
}
}
private synchronized void instance1() {
SleepTools.second(3);
System.out.println("SynInstance1 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance1 is ended..." + this);
}
private synchronized void instance2() {
SleepTools.second(3);
System.out.println("SynInstance2 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance2 is ended..." + this);
}
private static synchronized void synClass() {
SleepTools.second(3);
System.out.println("SynClass is going...");
SleepTools.second(3);
System.out.println("SynClass is ended...");
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread instance1Syn = new Thread(new Instance1Syn(synClzAndInst));
SynClzAndInst synClzAndInst2 = new SynClzAndInst();
Thread instance2Syn = new Thread(new Instance2Syn(synClzAndInst2));
instance1Syn.start();
instance2Syn.start();
SleepTools.second(10);
}
}
运行结果如下图:
结果说明:因为两个方法使用的对象锁 锁定的不是同一个对象(同一个类构造的不同的对象),所以两个线程可以同时执行。
2)将上面的代码进行修改,使得两个线程共用一个对象锁:
package top.jacktgq.ch1.syn;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: SynClzAndInst.java
* @Package top.jacktgq.ch1.syn
* @Description: 演示对象锁和类锁
* @author CandyWall
* @date 2019年10月8日 下午9:46:10
* @version V1.0
*/
public class SynClzAndInst {
private static class SynClass extends Thread {
@Override
public void run() {
System.out.println("TestClass is running...");
synClass();
}
}
private static class Instance1Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance1Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance1 is running..." + synClzAndInst);
synClzAndInst.instance1();
}
}
private static class Instance2Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..." + synClzAndInst);
synClzAndInst.instance2();
}
}
private synchronized void instance1() {
SleepTools.second(3);
System.out.println("SynInstance1 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance1 is ended..." + this);
}
private synchronized void instance2() {
SleepTools.second(3);
System.out.println("SynInstance2 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance2 is ended..." + this);
}
private static synchronized void synClass() {
SleepTools.second(3);
System.out.println("SynClass is going...");
SleepTools.second(3);
System.out.println("SynClass is ended...");
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread instance1Syn = new Thread(new Instance1Syn(synClzAndInst));
//SynClzAndInst synClzAndInst2 = new SynClzAndInst();
//Thread instance2Syn = new Thread(new Instance2Syn(synClzAndInst2));
Thread instance2Syn = new Thread(new Instance2Syn(synClzAndInst));
instance1Syn.start();
instance2Syn.start();
SleepTools.second(10);
}
}
运行结果如下图:
运行结果说明:因为两个方法使用的对象锁 锁定的是同一个对象,所以下一个线程会等待上一个线程全部执行完毕才会执行。
3)修改代码,使得一个线程使用对象锁,另外一个线程使用类锁:
package top.jacktgq.ch1.syn;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: SynClzAndInst.java
* @Package top.jacktgq.ch1.syn
* @Description: 演示对象锁和类锁
* @author CandyWall
* @date 2019年10月8日 下午9:46:10
* @version V1.0
*/
public class SynClzAndInst {
private static class SynClass extends Thread {
@Override
public void run() {
System.out.println("TestClass is running...");
synClass();
}
}
private static class Instance1Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance1Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance1 is running..." + synClzAndInst);
synClzAndInst.instance1();
}
}
private static class Instance2Syn implements Runnable {
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..." + synClzAndInst);
synClzAndInst.instance2();
}
}
private synchronized void instance1() {
SleepTools.second(3);
System.out.println("SynInstance1 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance1 is ended..." + this);
}
private synchronized void instance2() {
SleepTools.second(3);
System.out.println("SynInstance2 is going..." + this);
SleepTools.second(3);
System.out.println("synInstance2 is ended..." + this);
}
private static synchronized void synClass() {
SleepTools.second(3);
System.out.println("SynClass is going...");
SleepTools.second(3);
System.out.println("SynClass is ended...");
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread instance1Syn = new Thread(new Instance1Syn(synClzAndInst));
//SynClzAndInst synClzAndInst2 = new SynClzAndInst();
//Thread instance2Syn = new Thread(new Instance2Syn(synClzAndInst2));
Thread instance2Syn = new Thread(new Instance2Syn(synClzAndInst));
instance1Syn.start();
//instance2Syn.start();
SynClass synClass = new SynClass();
synClass.start();
SleepTools.second(10);
}
}
运行结果如下图:
运行结果说明:两个线程交替打印内容,说明两个线程使用的是不同的锁,所以可以同步执行。
4)volatile关键字修饰的变量,在进行非原子性操作的时候是不安全的,示例代码如下:
package top.jacktgq.ch1.vola;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: VolatileUnsafeDemo.java
* @Package top.jacktgq.ch1.vola
* @Description: 演示volatile无法提供操作的原子性
* @author CandyWall
* @date 2019年10月10日 下午4:35:18
* @version V1.0
*/
public class VolatileUnsafeDemo {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
String threadName = Thread.currentThread().getName();
a = a + 1;
System.out.println(threadName + ":=====" + a);
SleepTools.ms(100);
a = a + 1;
System.out.println(threadName + ":=====" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果如下图:
一次运行:
二次运行:
三次运行:
结果说明:运行的结果具有不可确定性,因此这种情况,volatile不能起到线程同步。
此时如果想要线程同步,需要用到synchronized关键字,修改后的代码如下:
package top.jacktgq.ch1.vola;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: SynchronizedSafeDemo.java
* @Package top.jacktgq.ch1.vola
* @Description: 使用synchronized关键字来完成刚才的volatile关键字无法完成的效果
* @author CandyWall
* @date 2019年10月10日 下午4:53:03
* @version V1.0
*/
public class SynchronizedSafeDemo {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
changeA();
}
private synchronized void changeA() {
String threadName = Thread.currentThread().getName();
a = a + 1;
System.out.println(threadName + ":=====" + a);
SleepTools.ms(100);
a = a + 1;
System.out.println(threadName + ":=====" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果如下图:
9、ThreadLocal的使用
示例代码:
package top.jacktgq.ch1;
/**
*
* @Title: UseThreadLocal.java
* @Package top.jacktgq.ch1
* @Description: 使用ThreadLocal类
* @author CandyWall
* @date 2019年10月10日 下午7:33:26
* @version V1.0
*/
public class UseThreadLocal {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行三个线程
*/
public void startThreadArray() {
Thread[] runs = new Thread[3];
for(int i = 0; i < runs.length; i++) {
runs[i] = new Thread(new TestRunnable(i));
}
for(int i = 0; i < runs.length; i++) {
runs[i].start();
}
}
/**
*
* @Title: UseThreadLocal.java
* @Package top.jacktgq.ch1
* @Description: 类说明:测试线程,该线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会相互影响
* @author CandyWall
* @date 2019年10月10日 下午7:59:18
* @version V1.0
*/
private static class TestRunnable implements Runnable {
int id;
public TestRunnable(int id) {
this.id = id;
}
@Override
public void run() {
Integer s = threadLocal.get(); //获得变量的值
System.out.println(Thread.currentThread().getName() + "===start:" + s);
s += id;
threadLocal.set(s);
System.out.println(Thread.currentThread().getName() + "===end:" + s);
}
}
public static void main(String[] args) {
new UseThreadLocal().startThreadArray();
}
}
运行结果如下图:
9.1 ThreadLocal是什么
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
从数据结构入手
下图为ThreadLocal的内部结构图
ThreadLocal结构内部
从上面的结构图,我们已经窥见ThreadLocal的核心机制:
每个Thread线程内部都有一个Map。
Map里面存储线程本地对象(key)和线程的变量副本(value)
但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
Thread线程内部的Map在类中描述如下:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
深入解析ThreadLocal
ThreadLocal类提供如下几个核心方法:
public T get()
public void set(T value)
public void remove()
get()方法用于获取当前线程的副本变量值。
set()方法用于保存当前线程的副本变量值。
initialValue()为当前线程初始副本变量值。
remove()方法移除当前前程的副本变量值。
get()方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
set()方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
步骤:
1.获取当前线程的成员变量map
2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
remove()方法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
remove方法比较简单,不做赘述。
9.2 ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
ThreadLocalMap类图
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
ThreadLocalMap的成员变量:
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
Hash冲突怎么解决
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
ThreadLocalMap的问题
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它业务逻辑
} finally {
threadLocal.remove();
}
应用场景
还记得Hibernate的session获取场景吗?
private static final ThreadLocal threadLocal = new ThreadLocal();
//获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
为什么?每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。
使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
总结:
每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。
二、线程间的协作
线程的协作可以使用轮询的方法,但是这种方式难以保证及时性,资源开销很大。因此引入了等待和通知的范式:
等待和通知的标准范式:
等待方:
(1)获取对象的锁;
(2)循环里判断条件是否满足,不满足调用wait方法;
(3)条件满足,执行业务逻辑
通知方:
(1)获取对象的锁
(2)改变条件
(3)通知所有等待在对象上的线程
1、wait()、notify()/notifyAll()的使用
wait():对象上的方法
notify()/notifyAll():对象上的方法
notify()和notifyAll()应该用谁?
应该尽量使用notifyAll(),因为使用notify有可能发生信号丢失的情况。
请看如下示例:
本例中模拟快递运输:
a.里程一旦超过100km,检测里程的线程就会打印”The km is 101,thread[xx] will change db.“;
b.城市一旦由ShangHai变为北京时,检测城市的线程就会打印“The site is BeiJing,thread[14] will call user.”。
快递实体类代码如下:
package top.jacktgq.ch1.wait_notify;
/**
*
* @Title: Express.java
* @Package top.jacktgq.ch1.wait_notify
* @Description: 快递实体类
* @author CandyWall
* @date 2019年10月11日 下午12:20:49
* @version V1.0
*/
public class Express {
public final static String CITY = "ShangHai";
private int km; //快递运输里程数
private String site; //快递到底地点
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/**
* 变化公里数,然后通知出于wait状态并需要处理公里数的线程进行业务处理
*/
public synchronized void changeKm() {
this.km = 101;
notifyAll();
}
/**
* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理
*/
public synchronized void changeSite() {
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm() {
while(this.km <= 100) {
try {
wait();
System.out.println("Check km thread[" + Thread.currentThread().getId() + "] is be notified!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("The km is " + this.km + ",thread[" + Thread.currentThread().getId() + "] will change db.");
}
public synchronized void waitSite() {
while(this.site.equals(CITY)) {
try {
wait();
System.out.println("Check site thread[" + Thread.currentThread().getId() + "] is be notified!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("The site is " + this.site + ",thread[" + Thread.currentThread().getId() + "] will call user.");
}
}
测试类如下:
package top.jacktgq.ch1.wait_notify;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: TestWaitAndNotify.java
* @Package top.jacktgq.ch1.wait_notify
* @Description: 测试wait和notify/notifyAll
* @author CandyWall
* @date 2019年10月11日 下午1:56:22
* @version V1.0
*/
public class TestWaitAndNotify {
private static Express express = new Express(0, Express.CITY);
/**
* 检查里程数变化的线程,不满足条件,线程一直等待
*/
private static class CheckKm extends Thread {
@Override
public void run() {
express.waitKm();
}
}
/**
* 检查地点变化的线程,不满足条件,线程一直等待
*/
private static class CheckSite extends Thread {
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) {
for(int i = 0; i < 3; i++) { //三个线程判断公里公里数变化
new CheckKm().start();
}
for(int i = 0; i < 3; i++) {
new CheckSite().start(); //三个线程判断地点变化
}
SleepTools.second(2); //线程休眠2秒
express.changeKm(); //改变快递里程数
//SleepTools.second(2);
//express.changeSite(); //改变快递地点
}
}
运行结果如下:
运行结果说明:本例中,在调用完将公里数改为101的方法然后调用了notify()方法,一共启动了3个检测公里数变化的线程,最后却只执行了1个,还有2个线程没有执行。原因是notify()被调用后,每次只会唤醒1个线程,所以还有2个线程没有被唤醒。这就造成了信号丢失。
如果把Express.java类中的notify()方法都改成notifyAll(),则所有线程都能正常执行。
代码如下:
package top.jacktgq.ch1.wait_notify;
import top.jacktgq.ch1.SleepTools;
/**
*
* @Title: TestWaitAndNotify.java
* @Package top.jacktgq.ch1.wait_notify
* @Description: 测试wait和notify/notifyAll
* @author CandyWall
* @date 2019年10月11日 下午1:56:22
* @version V1.0
*/
public class TestWaitAndNotify {
private static Express express = new Express(0, Express.CITY);
/**
* 检查里程数变化的线程,不满足条件,线程一直等待
*/
private static class CheckKm extends Thread {
@Override
public void run() {
express.waitKm();
}
}
/**
* 检查地点变化的线程,不满足条件,线程一直等待
*/
private static class CheckSite extends Thread {
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) {
for(int i = 0; i < 3; i++) { //三个线程判断公里公里数变化
new CheckKm().start();
}
for(int i = 0; i < 3; i++) {
new CheckSite().start(); //三个线程判断地点变化
}
SleepTools.second(2); //线程休眠5秒
express.changeKm(); //改变快递里程数
//SleepTools.second(2);
//express.changeSite(); //改变快递地点
}
}
运行结果如下图:
2、等待超时模式实现一个连接池
假设 等待时间为时长为T,当前时间now+T以后超时
该流程的伪代码实现如下:
long overtime = now + T;
long remain = T;//等待的持续时间
while(result不满足条件&&remain>0) {
}