线程基本使用入门
线程基本使用入门
-
错误的加锁与原因分析:synchronized锁的是对象,一定要保证锁的这个对象不能发生变化
-
重要函数:identityHashCode
-
作用:屏蔽对hashCode()的重写
- hashCode是Object中的成员方法,任何java类均可对其覆盖,
- 调用identityashCode后,即使对hashCode()进行了重写,返回的仍然是为未重写的hashCode
- hashCode可认为是对象的唯一标识符,是拿到对象在内存的位置后计算出来的(存在哈希冲突的问题)
-
源码:
public static native int identityHashCode(Object x);
-
-
为什么在执行i++操作后,成员变量i(加了锁的)的hashCode发生了改变
-
代码展示:
package cn.enjoyedu.ch1.syn; /** * 类说明:错误的加锁和原因分析 */ public class TestIntegerSyn { public static void main(String[] args) throws InterruptedException { Worker worker=new Worker(1); //Thread.sleep(50); for(int i=0;i<5;i++) { new Thread(worker).start(); } } private static class Worker implements Runnable{ private Integer i; public Worker(Integer i) { this.i=i; } @Override public void run() { synchronized (i) { Thread thread=Thread.currentThread(); System.out.println(thread.getName()+"--@"+ System.identityHashCode(i));//屏蔽hashCode覆盖(这个是对象在内存中的地址,排除哈希冲突) i++; System.out.println(thread.getName()+"-------"+i+"-@"+ System.identityHashCode(i)); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName()+"-------"+i+"--@"+ System.identityHashCode(i)); } } } }
-
运行结果:
-
寻找原因:
-
拿到程序的 .class文件,调用反编译工具进行查看
i++ 变成了:Integer.valueOf(this.i.intValue() + 1);
-
查看Integer.valueOf:在i++的时候会创建新的对象,对象都变了,当然锁不住
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
-
怎么解决synchronized(i)的问题?
-
锁this
-
实例化Object
Object o = new Object(); …… synchronized(o){ //同步代码块的内容 }
-
-
-
-
-
volatile关键字
-
概述:最轻量的同步机制,保证可见性
-
可见性:当线程修改volatile变量的值,保证这个新值被其他的线程看到
- 不加同步机制,还不一定能看到
-
-
可见性验证:
-
代码:不加volatile关键字
package cn.enjoyedu.ch1.vola; import cn.enjoyedu.tools.SleepTools; /** * 类说明:演示Volatile的提供的可见性 */ public class VolatileCase { private static boolean ready; private static int number; private static class PrintThread extends Thread{ @Override public void run() { System.out.println("PrintThread is running......."); while(!ready); System.out.println("number = "+number); } } public static void main(String[] args) { new PrintThread().start(); SleepTools.second(1); number = 51; ready = true; SleepTools.second(5); System.out.println("main is ended!"); } }
-
运行截图:主线程都结束了,子线程还在死循环,证明了主线程修改的变量在子线程中可见
-
代码:加上volatile关键字
package cn.enjoyedu.ch1.vola; import cn.enjoyedu.tools.SleepTools; /** * 类说明:演示Volatile的提供的可见性 */ public class VolatileCase { private volatile static boolean ready; private static int number; private static class PrintThread extends Thread{ @Override public void run() { System.out.println("PrintThread is running......."); while(!ready); System.out.println("number = "+number); } } public static void main(String[] args) { new PrintThread().start(); SleepTools.second(1); number = 51; ready = true; SleepTools.second(5); System.out.println("main is ended!"); } }
-
运行截图:子线程死循环退出了,证明了主线程修改的变量在子线程中可见
-
-
volatile能取代synchronized关键字吗?
-
不能,volatile只能保证可见性,并不能保证操作原子性(一个数据在多线程环境下的并发安全性)
-
应用场景:一写多读
-
只有一个线程写变量,多个线程读取这个变量
-
-
-
ThreadLocal:可以保证多线程的安全性
-
ThreadLocal与synchronized的区别:
-
synchronized:使用锁机制,保证在多线程环境下,同一时刻只能有一个线程访问某段代码/变量
-
ThreadLocal:为每一个线程提供变量的副本(只管理自己的)
-
实现了线程的隔离:每个线程只访问自己的数据,保证安全性
-
在Spring中的事务中使用了这个ThreadLocal
- 每个线程保存自己的链接,将链接绑在线程上作为参数
-
-
-
使用细节
- 可以对ThreadLocal初始化赋值
- 可以使用,get/set/remove进行操作
-
验证ThreadLocal为每一个线程提供变量的副本
-
代码:
package cn.enjoyedu.ch1.threadlocal; /** *类说明:演示ThreadLocal的使用 */ public class UseThreadLocal { //TODO private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){ //在new的时候可以进行初始化 @Override protected Integer initialValue() { return 1; } }; /** * 运行3个线程 */ public void StartThreadArray(){ Thread[] runs = new Thread[3]; for(int i=0;i<runs.length;i++){ runs[i]=new Thread(new TestThread(i)); } for(int i=0;i<runs.length;i++){ runs[i].start(); } } /** *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响 */ public static class TestThread implements Runnable{ int id; public TestThread(int id){ this.id = id; } public void run() { System.out.println(Thread.currentThread().getName()+":start"); //TODO Integer s = threadLocal.get();//取的时候可以用get s = s+id; threadLocal.set(s);//在放入的时候可以set,还可以调用remove()将其清除掉 System.out.println(Thread.currentThread().getName()+":"+ +threadLocal.get()); } } public static void main(String[] args){ UseThreadLocal test = new UseThreadLocal(); test.StartThreadArray(); } }
-
运行截图:
-
-
ThreadLocal的实现:Map(当前线程,Object)
-
工作原理:为每个变量保存副本
-
当来了一个新的线程,那么就为其创建一个独有(这个ThreadLocalMap是静态的)的ThreadLocalMap(ThreadLocal,object)//object就是要保存的东西
-
示意图:
-
-
get方法:
-
执行流程:
- 拿到当前线程
- 将当前线程传给了getMap
-
源代码:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);//拿到当前线程独有的ThreadLocalMap if (map != null) {//如果map不为空,去拿里面的entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
-
getMap:返回了一个当前线程的threadLocals
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
-
threadLocals:为ThreadLocal的成员变量ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
-
ThreadLocalMap(每个ThreadLocal对应唯一的ThreadLocalMap):内置了Entry<ThreadLocal <?> k,Object v),并且Entry是一个初始化大小为16的数组(因为有可能定义多个ThreadLocal< ? >)
-
当map不为空,去拿到map里面的 Entry数组(通过位运算)
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
-
-
set:内部会解决hash冲突
-
-
ThreadLocal内存泄漏分析:
-
使用JDK自带工具查看java进程内存变化:
-
代码:
package cn.enjoyedu.ch1.threadlocal; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 类说明:ThreadLocal造成的内存泄漏演示 */ public class ThreadLocalOOM { private static final int TASK_LOOP_SIZE = 500; //定义了大小为5的线程池 final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); static class LocalVariable { private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/ } final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>(); //在每一个Runnable任务中只是简单new了LocalVariable,同时申请了5MB的内存 public static void main(String[] args) throws InterruptedException { for (int i = 0; i < TASK_LOOP_SIZE; ++i) { poolExecutor.execute(new Runnable() { public void run() { new LocalVariable(); System.out.println("use local varaible"); } }); Thread.sleep(100); } System.out.println("pool execute over"); } }
-
内存分析:观察到就堆内存持续在25MB进行变化(五个线程同时运行,每个线程分配5MB)
-
修改代码:引入ThreadLocal,为每个线程保存一个变量的副本
public static void main(String[] args) throws InterruptedException { for (int i = 0; i < TASK_LOOP_SIZE; ++i) { poolExecutor.execute(new Runnable() { public void run() { localVariable.set(new LocalVariable()); System.out.println("use local varaible"); } }); Thread.sleep(100); } System.out.println("pool execute over"); }
-
内存监视:发现内存一度达到150MB+
-
再次修改代码:在set之后紧跟remove
public void run() { localVariable.set(new LocalVariable()); System.out.println("use local varaible"); localVariable.remove(); }
-
内存监视:发现这个跟未引入ThreadLocal效果一致
意思就是,引入了ThreadLocal造成了差不多70MB+的内存泄漏问题
-
-
为什么会发生内存泄漏?
-
查看ThreadLocalMap源码:发现使用了弱引用
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; }
-
当发生GC,ThreadLocal一定会被回收
-
为什么发生内存泄漏:数据不可达但是它真实存在
-
示意图:
-
流程分析:
上面那条线
-
一个ThreadLocal实例,对应了一个线程独有的(这个static的)ThreadLocalMap,KEY为ThreadLocal实例,value就是我们set进去的东西;堆中的ThreadLocal实例在栈上是有一个引用的(强)
-
一旦发生GC,这个ThreadLocal就会被回收,因为ThreadLocal被虚引用包裹,此时将栈上的ThreadLocal实例的引用置空(null),此时并不会回收Entry里面的value(之间是一条虚引用),但是map里面的key是ThreadLocal,这个都置空了,value就访问不到了
上面那条线就断了
下面那条线还存在
-
问题是在每一个线程中就有一个ThreadLocalMap,map里面又有一个Entry,这个Entry又指向了里面的value
-
但是KEY已经不见,相当于value存在但是无法引用,所以造成了内存泄漏
此时需要将当前线程一起回收
-
-
使用ThreadLocal的细节:set之后跟上remove
-
-
-
为什么,看起来分配了2500MB(一个线程此次分配5MB,共执行500次,并且共有5个线程),但是在内存检测过程中,对内内存稳定在25MB?get/set中还是会清除,但是不及时
-
回到get源码:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //这个里面有个getEntry(this) ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
-
getEntry(this):
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //这个里面有一个getEntryAfterMiss return getEntryAfterMiss(key, i, e); }
-
getEntryAfterMiss:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //这个里面还有个expungeStaleEntry方法,这个方法就是专门用来清除key为null的,ThreadLocalMap的; expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
-
在set方法中也有相关的东西
-
在remove里面也有相关的东西:
-
-
为什么JDK要定义成弱引用
-
如果使用强引用,那么一定会发生内存泄漏
- 定义成弱引用,还有机会可以回收,就是处理ThreadLocalMap中的key为null
- 定义成强引用的话,连这个检查的机会都没有
-
-
四大引用
-
强引用:正常new出的东西
- 执行方法的时候,方法打包成栈帧,对象实例在堆上分配,引用在栈上面,引用指向了堆中的对象实例
- 只要强引用存在,堆中的对象实例就不会被回收
-
软引用(SoftRefence):虽然指向了,但是虚拟机要发生OOM了,就回收掉,不管栈上有无引用指向堆中实例,发生GC后,内存够,就不收这个
-
弱引用(WeakReference):只要发生GC就一定会回收
-
虚引用:随时都会被回收
-
-
-
ThreadLocal的线程不安全性:处理静态变量的时候
-
多个线程调度静态变量,变量放在ThreadLocal里面去,此时发生线程不安全
-
代码:
package cn.enjoyedu.ch1.threadlocal; import cn.enjoyedu.tools.SleepTools; /** * 类说明:ThreadLocal的线程不安全演示 */ public class ThreadLocalUnsafe implements Runnable { public static Number number = new Number(0); public void run() { //每个线程计数加一 number.setNum(number.getNum()+1); //将其存储到ThreadLocal中 value.set(number); SleepTools.ms(2); //输出num值 System.out.println(Thread.currentThread().getName()+"="+value.get().getNum()); } public static ThreadLocal<Number> value = new ThreadLocal<Number>() { }; public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(new ThreadLocalUnsafe()).start(); } } private static class Number { public Number(int num) { this.num = num; } private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public String toString() { return "Number [num=" + num + "]"; } } }
-
运行截图:
-
原因:
虽然说每个线程都保存了变量的一个副本,在各自的线程中独立操作;但是在此处究其根本,保存的是对象引用,这些引用都指向了堆中的同一个实例(因为这个对象被静态修饰了)
-
解决办法一:去掉对象的static修饰
- 每个线程保存的就是各自的对象的引用,不会引起安全性问题
-
解决办法二:
-
在初始化的时候为ThreadLocal里面的泛型指定初始值
这个是没有解决的
-
-
-
-
线程相互协作:通知模式
-
场景:
- 线程A中对类属性做修改,线程B持续监测类属性,满足条件后触发
-
实现手段:
-
轮询:如果说线程B中有sleep函数,则会带来无端的资源浪费
-
使用wait()+notify():
-
线程B调用wait函数,当满足条件后,notify()线程B--->触发线程B
-
wait和notify与notifyAll不是Thread的方法是Object的方法(针对某个对象)
-
使用:一定要有synchronized包裹(不包裹,运行的时候就会抛异常)
//等待范式 sync(对象){ while(条件不满足){ 对象.wait()//调用wait()--->线程释放所持有的对象锁;当一个wait的线程被唤醒后,会去竞争锁,一旦拿到锁,接着就检查条件,条件满足,就进行业务逻辑 业务逻辑 } //条件满足 具体的业务逻辑代码 } //通知范式 sync(对象){ //业务逻辑,改变条件 对象.notify/notifyAll();//调用后不会释放持有的对象锁 //业务逻辑2,等到这个同步代码块执行完后,才释放对象锁 这个notify/notifyAll会放在同步代码块的最后一行,因为它不释放对象锁 }
-
-
-
-
-
wait与notify()实战
-
定义了两个类:Express,TestWN
- Express:这里面就有两个线程(等待公里数变化,等待地点变化),两种唤醒
- TestWN:
-
实现细节:
-
关于notify与notifyAll的使用:一般情况下都是使用notifyAll
- notify:只会唤醒,这个持有这把锁的任意一个线程(不能确定,唤醒信号不能向下传递)
- notifyAll:唤醒所有持有这把锁的对象
-
关于notify与notifyAll,wait
-
是在对象上面等待和唤醒,需要注意如果等待的条件是类的属性,这个时候就会要注意细节
-
一个对象一个条件就可以用notify
-
一个对象多个条件用notifyAll
- 或者直接去锁那个条件,但是这个是有点问题的,需要确保对象不改变,那种i++就锁不到
-
- 必须在同步代码块中
- wait函数后需要对异常进行处理:JDK对于内在阻塞方法都是具有中断机制的
-
-
-
代码示意:
Express:
package cn.enjoyedu.ch1.wn; /** *类说明:快递实体类 */ 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(){ //TODO this.km = 101; notifyAll();//使用notify是不行的;唤醒了等待地点的线程,等待公里数没有被触发,因为 } /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/ public synchronized void changeSite(){ this.site = "BeiJing"; notifyAll(); } //线程等待公里数的变化,当公里数小于100就不管 public synchronized void waitKm(){ //TODO while(this.km<100){ try { wait(); System.out.println("Check Site thread[" +Thread.currentThread().getId() +"] is be notified"); } catch (InterruptedException e) {//所有JDK实现的阻塞方法,都会有中断处理机制 e.printStackTrace(); } } System.out.println("the Km is "+this.km+",I will change db"); } //等待地方的变化,当这个地点发生变化后进行处理 public synchronized void waitSite(){ while(this.site.equals(CITY)){//快递到达目的地,这个地方使用while(当条件不满足时,这个线程继续休眠一直等到条件满足后,触发通知) //用了if,它就只搞了一次了 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+",I will call user"); } }
TestWN
package cn.enjoyedu.ch1.wn; /** *类说明:测试wait/notify/notifyAll */ public class TestWN { 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) throws InterruptedException { for(int i=0;i<3;i++){ new CheckSite().start(); } for(int i=0;i<3;i++){ new CheckKm().start(); } Thread.sleep(1000); express.changeKm();//快递地点变化,这个时候使用notify, // 会唤醒express对象实例加锁的任意一个线程(连条件都不能确定,并且唤醒后,唤醒信号就丢失了),notifyAll是全部唤醒 } }
-
-
转载自:https://juejin.cn/post/7067235972215209992