线程解决方案
1、 加锁
加锁⽅案虽然可以正确的解决线程不安全的问题,但同时也引⼊了新的问题,加锁会让程序进⼊排 队执⾏的流程,从⽽⼀定程度的降低了程序的执⾏效率,那有没有⼀种⽅案既能解决线程不安全的 问题,同时还可以避免排队执⾏呢?
2、ThreadLocal(ThreadLocal 线程本地变量 :线程级别的私有变量,与任务级别的私有变量完全不同)
ThreadLocal经典实用场景:
1、解决线程安全问题
2、实现线程级别的数据传递
ThreadLocal使用
1、set(T):将私有变量存放到线程中
2、get():从线程中取得私有变量
3、remove():从线程中移除私有变量(如果不进行remove操作,会造成脏读、内存溢出)
4、initialValue:初始化方法1
5、withInitial:初始化方法2
ThreadLocal 基础⽤法如下:
public class ThreadPoolLocal1 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//set
String tname = Thread.currentThread().getName();
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",tname,tname));
printTname();
}
},"线程1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//set
String tname = Thread.currentThread().getName();
threadLocal.set(tname);
System.out.println(String.format("线程:%s 设置了值:%s",tname,tname));
printTname();
}
},"线程2");
t2.start();
}
private static void printTname() {
String tname = Thread.currentThread().getName();
//得到存放在ThreadLocal中的值
}
}
initialValue的用法
public class ThreadPoolLocal2 {
private static ThreadLocal<String> threadLocal = new ThreadLocal(){
@Override
protected String initialValue() {
System.out.println("执行了initialValue方法");
return "默认值";
}
};
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
print(threadName);
}
};
new Thread(runnable,"t1").start();
new Thread(runnable,"t2").start();
}
//打印threadLocal中的值
private static void print (String threadName){
String result = threadLocal.get();
System.out.println("线程:" + threadName + "得到值:" + result);
}
}
initialValue + get
正常存取现象
initialValue + set + get?
先执行set方法,在执行get方法
什么情况下不会执行InitialValue?为什么不会?
答:set之后就不会执行了
ThreadLocal是懒加载,当调用了get方法之后,才会尝试执行InitialValue(初始化)方法,尝试获取一下ThreadLocal set的值,如果获取到了值,那么初始化方法永远不会执行。
注意在使⽤ initialValue 时,返回值的类型要和 ThreadLoca 定义的数据类型保持⼀致。
如果数据不⼀致就会造成 ClassCaseException 类型转换异常,如下图所示:
withInitial的用法
import java.util.function.Supplier;
public class ThreadPoolLocal3 {
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了初始化方法");
return "默认值";
}
});
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
print(threadName);
}
};
new Thread(runnable,"t1").start();
new Thread(runnable,"t2").start();
}
//打印threadLocal中的值
private static void print (String threadName){
String result = threadLocal.get();
System.out.println("线程:" + threadName + "得到值:" + result);
}
}
好像跟initialValue没什么区别?
别急!withInitial还有一种很简洁的方法。
public class ThreadPoolLocal3 {
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(()->"默认值");
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
print(threadName);
}
};
new Thread(runnable,"t1").start();
new Thread(runnable,"t2").start();
}
//打印threadLocal中的值
private static void print (String threadName){
String result = threadLocal.get();
System.out.println("线程:" + threadName + "得到值:" + result);
}
}
是不是更方便了呢?
ThreadLocal的缺点
1、不可继承性
子线程中不可读取到父线程的值。
public class ThreadPoolLocal9 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
String data = "java";
System.out.println("主线程存入数据:" + data);
threadLocal.set(data);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("子线程读取数据:"+result);
}
});
t1.start();
}
}
如何解决不可继承性?
InheritableThreadLocal
public class ThreadPoolLocal9 {
private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
String data = "java";
System.out.println("主线程存入数据:" + data);
threadLocal.set(data);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("子线程读取数据:"+result);
}
});
t1.start();
}
}
不能实现不同线程之间的数据共享。
public class ThreadPoolLocal9 {
private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
String data = "java";
threadLocal.set(data);
String result = threadLocal.get();
System.out.println("t1存入数据:"+ result);
}
});
t1.start();
t1.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("t2线程得到数据:" + result);
}
});
t2.start();
}
}
为什么会出现这种情况?
因为⽆论是 ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变 量,所以不能跨线程进⾏数据共享也是正常的。
2、脏数据
在一个线程中读取到了不属于自己的数据。
!!!线程使用ThreadLocal不会出现脏读,因为每个线程使用的是自己的变量值和ThreadLocal。
在使⽤线程池的时候,因为线程池会复⽤线程,那么和线程绑定的静态属性也会被复⽤(如: ThreadLocal),所以如果下⼀个线程不执⾏ set 操作或者上⼀个线程不执⾏ remove 操作,那么下⼀个 线程就可以读取到旧值,如下代码所示:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolLocal10 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//创建线程为一个的线程池
ExecutorService service = Executors.newFixedThreadPool(1);
for (int i = 0; i < 2; i++) {
MyThread myThread = new MyThread();
service.execute(myThread);
}
service.shutdown();
}
private static class MyThread extends Thread{
private static boolean flag = true;
@Override
public void run() {
if (flag){
threadLocal.set(this.getName() + " session info");
flag = false;
}
System.out.println(this.getName() + ":" + threadLocal.get());
}
}
}
脏数据的解决方案:
1、避免使用静态属性(静态属性在线程池中会复用)
2、使用remove解决。
3、内存溢出问题(最常出现的问题)
内存溢出:当一个线程执行完之后,不会释放线程所占用的内存或者释放内存不及时的情况都叫着内存溢出。线程不用了,单线程线程相关的内存还得不到及时的释放。
线程和线程池使用ThreadLocal的的区别:产生OOM异常
原因:ThreadLocal + 线程池(线程池是长生命周期的),而线程是执行完任务线程就结束了(线程相关的资源都会释放掉)。
为什么会发生OOM?
ThreadPool(长生命周期)-》Thread-》ThreadLocalMap-》Entry[]-》Entry-》key(弱引用),value(强引用)
为什么将ThreadLocal中的key设置成弱引用?
答:为了最大程度的避免OOM。
如何解决ThreadLocal的溢出?
使用remove()。
这里先演示一下:
将线程最大内存改为5m
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocal2 {
static class OOMObject{
//1m大小的对象
private byte[] bytes = new byte[1*1024*1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
OOMObject oomObject = new OOMObject();
//set threadlocal
System.out.println("任务:" + finalI + "执行了");
threadLocal.set(oomObject);
//不用对象了
oomObject = null;
}
});
Thread.sleep(200);
}
}
}
这里可以看到,当执行了三个任务之后,就发生了OOM异常,因为主线程也占用了一定内存,所以它根本不可能执行完五个任务。
我们再来看看使用remove方法之后的结果:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocal3 {
static class OOMObject{
//1m大小的对象
private byte[] bytes = new byte[1*1024*1024];
}
static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {
int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
OOMObject oomObject = new OOMObject();
//set threadlocal
System.out.println("任务:" + finalI + "执行了");
threadLocal.set(oomObject);
//不用对象了
oomObject = null;
}finally {
threadLocal.remove();
}
}
});
Thread.sleep(200);
}
}
}
可以看到五个任务都执行完了。
HashMap和ThreadLocalMap处理hash冲突的区别:
HashMap使用的链式法,ThreadLocalMap使用的开放寻址法
为什么要这么实现?
答:开放寻址法的特点是:数据量比较少的情况下,性能更好;
而HashMap存储的数据通茶情况下是比较多的,使用开放寻址法效率就比较低了,这个时候最好使用链式法。
升级:链表-》红黑树
1、链表长度大于8
2、数组的长度大于64
降级:红黑树-》链表
1、当链表的长度小于6