调度策略:
Java的调度方法:
等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级
注意事项:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到JVM关闭。
又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。不会常驻内存,当实例对象被JVM 回收之后,也跟着消失。
线程非安全,静态变量即类变量,位于方法区,为所有该类下的对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
线程安全,每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性。如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量。
Java多线程里对于共享变量的操作往往需要考虑进行一定的同步互斥操作,原来是因为Java内存模型导致的共享内存对于线程不可见。
Java 内存模型规定,将所有的变量都存放在主内存中。
多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作内存中成为一个副本,对这个副本进行改动之后,再更新回主内存中变量所在的地方。
由于CPU时间片是以线程为最小单位,所以这里的工作内存实际上就是指的物理缓存,CPU运算时获取数据的地方;而主内存也就是指的是内存,也就是原始的共享变量存放的位置。
一个线程A对共享变量1的修改对线程B可见,需要经过下列步骤:
要实现共享变量的可见性必须保证下列两点:
一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。
1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。
2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
Java中可以通过synchronized、volatile、java concurrent类来实现共享变量的可见性。
使用synchronized可以保证原子性(synchronized代码块内容要么不执行,要执行就保证全部执行完毕)和可见性,修改后的代码为在write和read方法上加synchronized关键字。
JMM关于Synchronized的两条规定:
synchronized 实际上是对访问修改共享变量的代码块进行加互斥锁,多个线程对synchronized代码块的访问时,某一时刻仅仅有一个线程在访问和修改代码块中的内容(加锁),其他所有的线程等待该线程离开代码块时(释放锁)才有机会进入synchronized代码块。
某一个线程进入synchronized代码块前后,执行过程入如下:
随后,其他代码在进入synchronized代码块的时候,所读取到的工作内存上共享变量的值都是上一个线程修改后的最新值。
注意,synchronized加锁后用到的变量才会从主内存拉取、才会修改后刷新回主内存。
特别情况:
在上个例子的基础上加上一段代码:System.out.println(isStop),查看打印日志:
没有达到自己预期的效果:结束前一次貌似应该打印true;
上面代码其实有两种结果:
volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存。这样一来,不同的线程都能及时的看到该变量的最新值。
但是volatile不能保证变量更改的原子性
比如number++,这个操作实际上是三个操作的集合(读取number,number加1,将新的值写回number),volatile只能保证每一 步的操作对所有线程是可见的,但是假如两个线程都需要执行number++,那么这一共6个操作集合,之间是可能会交叉执行的,那么最后导致number 的结果可能会不是所期望的。
举例说明:
程序不会进入死循环,原因:
isStop是被volatile修饰的,所有每次while时都是从主内存中获取isStop的值,当子线程2秒钟后修改了isStop的值为true,并刷新进了住内存,此后while从主内存中获取的isStop值为true,结束循环。
AtomicInteger:一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
volatile不需要同步操作,所以效率更高,不会阻塞线程,但是适用情况比较窄
volatile读变量相当于加锁(即进入synchronized代码块),而写变量相当于解锁(退出synchronized代码块)
synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性
对变量的写入操作不依赖当前值
比如自增自减、number = number + 5等(不满足)
当前volatile变量不依赖于别的volatile变量
比如 volatile_var > volatile_var2这个不等式(不满足)
以上操作会从主内存拉去值到工作内存,所有如果在上面的例子的weile循环中有这些操作,不会造成死循环。
多线程之间就是因为数据共享在多个线程才导致了线程不安全,这就要求线程间的数据需要隔离,从根本上解决了线程安全问题。
提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。
主要是initialValue、set、get、remove这几个方法
Thread、ThreadLocal、ThreadLocalMap三者的关系:
每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal。
get方法
get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。
注意:这个map以及map中的key和value都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中
getMap方法:
获取到当前线程内的ThreadLocalMap对象。每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null。
set方法
把当前线程需要全局共享的value传入
initialValue方法
这个方法没有默认实现,如果要用initialValue方法,需要自己实现,通常使用匿名内部类的方式实现
remove方法
删除对应这个线程的值。
ThreadLocalMap类
ThreadLocalMap类,也就是Thread.threadLocals。
// 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 类是每个线程Thread类里面的变量,但ThreadLocalMap这个静态内部类定义在ThreadLocal类中,其中发现这一行代码
private Entry[] table;
里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:
这个思路和HashMap一样,那么我们可以把它想象成HashMap来分析,但是实现上略有不同。
比如处理冲突方式不同,HashMap采用链地址法,而ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry
1.ThreadLocal内存泄漏问题
ThreadLocalMap中的Entry继承自 WeakReference,是弱引用,ThreadLocal可能出现Value泄漏!
什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
弱引用:通过WeakReference类实现的,在GC的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal中的key就用到了弱引用,有利于内存回收。
强引用:我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象。
ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时,每个 Entry 都包含了一个对value的强引用,如下:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k); // key值给WeakReference处理
value = v; // value直接用变量保存,是强引用
}
}
正常情况下,当线程终止,保存在ThreadLocalMap里的value会被垃圾回收,因为没有任何强引用了。但如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:
Thread---->ThreadLocalMap---->Entry(key为null,弱引用被回收)---->value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。
JDK已经考虑到了这个问题,所以在set, remove, rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。比如rehash里面调用resize,如果key回收了,那么value也设置为null,断开强引用链路,便于垃圾回收。
private void resize() {
......省略代码
ThreadLocal> k = e.get();
if (k == null) {
e.value = null; // Help the GC
}
......
但是如果一个ThreadLocal不被使用,那么实际上set, remove, rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏。
ThreadLocal如何避免内存泄漏
及时调用remove方法,就会删除对应的Entry对象,可以避免内存remove泄漏,所以使用完ThreadLocal之后,应该调用remove方法。
比如拦截器获取到用户信息,用户信息存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove清除User对象,这样就能稳妥的保证不会内存泄漏。
参考文章:
Java多线程里共享变量线程安全问题的原因
Java多线程共享变量控制
Java多线程超详解
ThreadLocal详解