多线程并发总结录(二)--线程间共享
线程间的共享
1. 内置锁Synchronized
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
1.1 Synchronized的用法和用处
用处:有多个线程会修改到某个属性的地方,需要对修改处加锁,保证每次只有一个线程可以修改。
1.2 对象锁
对象锁是用于对象实例方法,或者一个对象实例上的。但是需要注意的是,被锁的对象不能发生改变,更不能创建对象,因为这会导致锁失效。不同对象的锁可以同时操作同一属性。
1.3 类锁
类锁其实锁的是每个类对应的class 对象,类锁是用于类的静态方法或者一个类的class 对象上的
2. 轻量级锁volatile的用法和使用场景
volatile关键字可以保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
volatile虽然保证了操作可见性,但是不能保证变量在多线程操作下的线程安全。所以volatile的使用场景是:只有一个线程写,多个线程读的场景。
3. ThreadLocal的辨析
3.1 ThreadLocal的使用
public class ThreadLocalTest {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
/**
* 运行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;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
threadLocal.set("线程"+id);
if(id==1) {
// 只有当是线程1的时候才会执行
threadLocal2.set(id);
System.out.println(threadName+":"+threadLocal2.get());
}
System.out.println(threadName+":"+threadLocal.get());
}
}
public static void main(String[] args){
ThreadLocalTest test = new ThreadLocalTest();
test.StartThreadArray();
}
}
运行结果:
Thread-0:线程0
Thread-2:线程2
Thread-1:1
Thread-1:线程1
从结果可以看出,threadLocal在每个线程中都完成了安全的赋值,threadLocal2在线程1完成了线程安全的赋值。
究竟ThreadLocal是如何保证线程安全的呢?
先说说结论:通过每个线程使用ThreadLocal的副本数据才保证线程安全的。意思是每个线程都会拿到threadLocal的初始值,然后在自己线程中备份一个这个值,当对这个值进行操作的时候,各自线程使用各自备份的这个值,其他线程无法修改自己线程的值,所以保证了线程安全。
3.2 ThreadLocal实现解析
//java.lang.ThreadLocal中
public T get() {
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿到当前线程类中的threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从map中拿到Entry, key是当前的threadlocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 从entry中拿到值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//java.lang.Thread中
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
从以上代码和如下ThreadLocal图解可知道,每一个线程都持有一个ThreadLocalMap对象,ThreadLocalMap中保存Entry对象,其中每一个Entry都包括<Key,Value>键值对,键是用户建的ThreadLocal对象,值是初始化值。然后当线程需要处理到ThreadLocal中的值时,每一个线程会将值拷贝一份到线程中进程独自操作这个值,从而实现了线程安全。
ThreadLocal图解.png3.3 ThreadLocal引发的泄露问题
(坚持三个原则:发现问题,定位问题,解决问题)
发现问题:
运行一下代码(别释放remove注释),设置一下堆区大小,很快就会发现OOM了。
public class MemoryLeak {
static Executor executor = new ScheduledThreadPoolExecutor(5);
static ThreadLocal<LocalValue> threadLocal = new ThreadLocal<>();
static class LocalValue {
private byte[] a = new byte[1024 * 1024 * 10];
}
public static void main(String[] args) {
for (int i = 0; i < 500; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
threadLocal.set(new LocalValue());
System.out.println("use thread local");
// threadLocal.get(); 以下是使用threadlocal
// ....使用代码
// threadLocal.remove();
}
});
}
}
}
定位问题:
结合下图和从3.2分析可以看到,当前线程是会持有ThreadLocalMap对象,虽然map中key持有的threadlocal对象,他是弱引用,在GC的时候会被回收,但是Value值是强应用,在GC的时候,只要线程没有运行结束,value对象不会被释放。所以在线程里一直创建对象,就会导致内存泄漏。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal内存泄漏问题.png
其实我们在测试的过程中会发现只会泄漏一部分内存,原因是什么呢?
查看ThreadLocal中方法调用栈:
get() -> replaceStaleEntry() -> expungeStaleEntry()
set() -> replaceStaleEntry() -> expungeStaleEntry()
remove() -> expungeStaleEntry()
结论:从调用来看,get、set、remove最终都会调用到expungeStaleEntry(),expungeStaleEntry()会删除map中key为null的节点。但是每次get和set不会立马调用,所以才会导致泄漏一部分。
解决问题:
因为remove()方法会立马调用到expungeStaleEntry()来清除key为空的过时条目。所以在使用完成之后,最好调用一下remove()方法,尽快回收不用内存空间(如上面代码屏蔽掉的代码)。
3.4 ThreadLocal的线程不安全
运行如下代码可以发现如果Number声明成静态对象,到导致线程不安全。因为静态对象在堆空间中只有一份,每次修改之后ThreadLocal在每个线程中备份的那份都是随线程修改这个值一直改变的,所以会存在线程不安全。正确做法:声明放在threadlocal中的对象不能是静态的即可。
public class ThreadLocalUnsafe implements Runnable {
// ThreadLocal使用静态的对象作value,会导致线程不安全
// public static Number number = new Number(0);
public Number number = new Number(0);
@Override
public void run() {
//每个线程计数加一
number.setNum(number.getNum()+1);
//将其存储到ThreadLocal中
value.set(number);
//输出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 + "]";
}
}
}
3.5 Synchronized和ThreadLocal的区别
ThreadLocal是一个线程隔离的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现;
synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。
虽然两个实现线程安全的手段不同,设计初衷也不同,没有可比性。
但是我还是想简单的总结一下:synchronized实现线程安全的方案是 时间换空间的方案;而ThreadLocal实现线程安全的方式是空间换时间的方案