线程的同步和死锁
synchronized(同步锁)
同一个对象多个线程同时操作,如果只是查看不会影响到数据,但如果要针对数据进行修改会导致出现数据异常情况。
同步锁又分为,同步方法和同步块;
两种的区别在于锁定的范围大小,间接的影响到安全性以及效率问题。
线程死锁
我们使用了同步锁解决并发情况也产生了特殊的情况—死锁:
多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上的对象锁”时,就可能发生“死锁”。
public class DeadLock implements Runnable {
private Jian jj = new Jian() ;
private XiaoQiang xq = new XiaoQiang() ;
@Override
public void run() {
jj.say(xq);
}
public DeadLock() {
new Thread(this).start();
xq.say(jj);
}
public static void main(String[] args) {
new DeadLock( ) ;
}
}
class Jian {
public synchronized void say(XiaoQiang xq) {
System.out.println("阿健说:此路是我开,要想从此过,留下10块钱。");
xq.get();
}
public synchronized void get() {
System.out.println("阿健说:得到了10块钱,可以买饭吃了,于是让出了路。");
}
}
class XiaoQiang {
public synchronized void say(Jian jj) {
System.out.println("小强说:让我先跑,我再给你钱。");
jj.get();
}
public synchronized void get() {
System.out.println("小强说:逃过了一劫,可以继续送快餐了。");
}
}
ThreadLocal
ThreadLocal是什么?有哪些用途?
ThreadLocal是线程Thread中属性threadLocals的管理者。也就是说我们对于ThreadLocal的get, set,remove的操作结果都是针对当前线程Thread实例的threadLocals存,取,删除操作。
总结:
- 线程并发:在多线程并发的场景下;
- 传递数据:我们可以通过Thr eadLocal在同一线程,不同组件中传递公共变量;
- 线程隔离:每个线程的变量都是独立的,不会互相影响。
常用方法:
ThreadLocal():创建ThreadLocal对象
public void set( T value):设置当前线程绑定的局部变量
public T get():获取当前线程绑定的局部变量
public void remove():移除当前线程绑定的局部变量
ThreadLocal和synchronized区别
synchronized:
原理:同步机制采用以“时间换空间”的方式,只提供了一份变量,让不同的线程排队访问;
侧重点:多个线程之间访问资源的同步。
ThreadLocal:
原理:ThreadLocal采用"以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰;
侧重点:多线程中让每个线程之间的数据相互隔离。
ThreadLocal—关于Spring框架底层
Spring中我们可以看到,在一些特定场景下 , ThreadLocal方案有两个突出的优势:
传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题;
线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
ThreadLocal原理
核心方法的源码:
public void set( T value) 源码:
代码执行流程:
首先获取当前线程,并根据当前线程获取一个Map;
如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key );
如果Map为空,则给该线程创建Map ,并设置初始值。
云设置当前线程对应的ThreadLocal的值
* @param value 将要保存在当前线程对应的ThreadLoca1的值
*/
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread() ;
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != nu11)
//存在则调用map. set设置此实体entry
map.set(this,value);
else
// 1)当前线程Thread不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t,value);
/**
*获取当前线程Thread对应维护的ThreadLocalMap
*@param t the current thread 当前线程
* @return the map对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
*创建当前线程Thead对应维护的ThreadLocalMap
* @paramt当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t,T firstValue) {
//这里的this:是调用此方法的threadLocal
t.threadlocals = new ThreadLocalMap(this,firstValue);
}
public T get() 源码:
代码执行流程
首先获取当前线程,根据当前线程获取一个Map
如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entrye ,否则转到最后一步;
如果e不为null ,则返回e.value ,否则转到最后一步;
Map为空或者e为空,则通过initialValue函数获取初始值value ,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一 个新的Map
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。
/**
返回当前线程中保存ThreadLocal的值
如果当前线程没有此ThreadLocal变量,
则它会通过调用{@link #initialvalue} 方法进行初始化值
@return 返回当前线程对应此Thr eadL oca1的值
*/
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread() ;
//获取此线程对象中维护的Thr eadLocalMap对象
ThreadLocalMap map = getMap(t) ;
//如果此map存在
if (map != nu11) {
//以当前的ThreadLocal 为key,调用getEntry获取对应的存储实体e
Thr eadLocalMap.Entry e = map. getEntry(this);
//对e进行判空
if (e != nu11) {
@Suppr essWarnings ("unchecked")
//获取存储实体e对应的value值
//即为我们想要的当前线程对应此ThreadL oca1的值
T result = (T)e.value;
return result;
}
/**
*初始化
* @return the initial value 初始化后的值
*/
private T setInitialvalue(){
//调用initialvalue获取初始化的值
//此方法可以被子类重写,如果不重写默认返回nu11
T value = initialvalue();
//获取当前线程对象
Thread t = Thread.currentThread( ;
//获取此线程对象中维护的Thr eadL ocalMap对象
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != nu11)
//存在则调用map. set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread不存在Thr eadLocalMap对象
// 2)则调用creat eMap进行Thr eadLocalMap对象的初始化
// 3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至 Thr eadLocalMap中
createMap(t, value) ;
//返回设置的值value
return value;
}
public void remove() 源码:
代码执行流程
首先获取当前线程,并根据当前线程获取一个Map;
如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
*删除当前线程中保存的ThreadLoca1对应的实体entry
*/
public void remove() {
//获取当前线程对象中维护的Thr eadLocalMap对象
ThreadLocalMap m = getMap (Thread.currentThread));
//如果此map存在
if (m != nu11)
//存在则调用map. remove
//以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
public T initialvalue() 源码:
此方法的作用是返回该线程局部变量的初始值;
这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
这个方法缺省实现直接返回一个null。
如果想要一个除null之外的初始值,可以重写此方法。( 备注:该方法是一个protected的方法,显然是为让子类覆盖而设计的)
/**
*返回当前线程对应的ThreadLocal的初始值
*此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLoca值时
台除非线程先调用了set方法,在这种情况下,initialvalue才不会被这个线程调用。
*通常情况下,每个线程最多调用一次这个方法。
* <p>这个方法仅仅简单的返回null {@code null};
*如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
*必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
*通常,可以通过匿名内部类的方式实现
* @return 当前ThreadLocal的初始值
*/
protected T initialvalue() {
return null;
}
内部结构:
每个Map存储的Entry数量变少;
当Thread销毁的时候, ThreadLocalMap也会随之销毁,减少内存的使用。
ThreadLocalMap的基本结构:
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
/**
*初始容量--必须是2的整次幂
*/
private static final int INITIAL_ CAPACITY = 16;
/**
*存放数据的table,Entry类的定义在下面分析
*同样,数组长度必须是2的整次幂。
*/
private Entry[] table;
/**
*数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
*/
private int size = 0;
/**
音进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
HashMap类似, INITIAL CAPACITY代表这个Map的初始容量; table 是一个Entry 类型的数组,用于存储数据; size代表表中的存储数目;
threshold 代表需要扩容时对应size的阈值。
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
/*
*Entry继承WeakReference ,并且用ThreadLocal作为key.
*如果key为nu11(entry.get() == null) ,意味着key不再被引用,
*因此这时候entry也可以从table中清除。
*/
static class Entry extends WeakReference <ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
object value;
Entry(ThreadLocal<?> k,object v) {
super(k) ;
value = v;
}
}
ThreadLocal内存泄漏
内存泄漏相关概念:
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Menory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序末释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
弱引用相关概念:
Java中的引用有4种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用;
强引用( StrongReference) , 就是我们最常见的普通对象引用,只要还有强引用指向一一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用( WeakReference) , 垃圾回收器一旦发现了 只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果key使用强引用和弱引用会发生内存泄漏吗:
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:
为什么使用弱引用:
我们知道了:无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
使用完ThreadLocal ,调用其remove方法删除对应的Entry;
使用完ThreadLocal ,当前Thread也随之运行结束。
那么为什么key要用弱引用呢?
在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
生产者与消费者
生产者与消费者基本程序模型
- 生产者负责负责信息内容的生产;
- 每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息;
- 如果生产者没有生产者则消费者要等待它生产完成,如果消费者还没有对信息进行处理,则生产者应该要等待消费处理完成后再继续生产。
线程的等待与唤醒
等待机制:
死等: public final void wait( )throws InterruptedException;
设置等待时间: public final void wait(long timeout)throws InterruptedException;
设置等待时间: public final void wait(long timeout, int nanos)throws InterruptedException;
唤醒第一个等待线程: public final void notify();唤醒第一个等待线程
唤醒全部等待线程: public final void notifyAll();唤醒所有的等待线程,哪个线程优先级高就先
class Producer implements Runnable {
private Message msg ;
public Producer(Message msg) {
this.msg = msg ;
}
@Override
public void run() {
for(intX=0;x<100;X++){
if (x % 2==0){
this.msg.set("王健","宇宙大帅哥");
} else {
this.msg.set("小高","猥琐第一人");
}
}
}
}
class Consumer implements Runnable {
private Message msg ;
public Consumer(Message msg) {
this.msg = msg ;
@Override
public void run() {
for(intx=0;x<100;x++){
System.out.println(this.msg.get());
}
}
}
class Message {
private String title ;
private String content ;
private boolean flag;
//flag=true 允许生产,不允许消费
//flag=false 允许消费,不允许生产
public synchronized void set(String content,String title) {
if(this.flag==false){ 允许消费,不允许生产
super.wait();
}
try{
this.content = content ;
this.title = title;
}finally{ //不管如何都要执行
this.flag==false // 允许消费
super.notify(); //唤醒
}
}
public synchronized String get() {
if(this.flag==true){ 允许生产,不允许消费
super.wait();
}
try{
return this.content+ '_' + this.title;
}finally{ //不管如何都要执行
this.flag==true; //继续生产
super.notify(); //唤醒
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
new Thread(new Producer(msg)).start(); // 启动生产者线程
new Thread(new Consumer(msg)).start(); // 启动消费者线程
}
}
多线程
后台守护线程
如果主线程或其他线程还在执行,守护线程一直存在,并且运行在后台状态;
在Thread类里面提供有如下的守护线程的操作方法:
设置为守护线程: public final void setDaemon( boolean on);
判断是否为守护线程: public final boolean isDaemon();
public class ThreadDemo {
public static boolean flag = true;
public static void main(String[] args) throws Exception {
Thread userThread = new Thread(() -> {
for (int X =0;x< Integer.MAX_ VALUEI; X ++) {
try {
Thread.sLeep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在运行、X = "+ x);
}
}, "用户线程"); //完成核心的业务
Thread daemonThread = new Thread(() -> {
for(intX=0;X<Integer.MAX_VALUE;x++){
try {
Thread.sLeep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在运行、x="+x);
}
},"守护线程"); // 完成核心的业务
daemonThread.setDaemon(true); // 设置为守护线程
userThread.start();
daemonThread.start();
}
volatile关键字
在多线程的定义之中,volatile关键字主要是在属性定义上使用,表示此属性为直接数据操作,而不进行副本的拷贝处理,不能错误的理解为同步属性;如果使用了volatile直接内存操作,省略掉副本的拷贝处理。
请解释volatile 与synchronized 的区别?
volatile主要在属性上使用,而synchronized是在代码块与方法上使用的;
volatile无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,synchronized是起到同步的作用。