内置锁Synchronized
1
| Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
|
Synchronized的用法和用处
1
| 用处:有多个线程会修改到某个属性的地方,需要对修改处加锁,保证每次只有一个线程可以修改。
|
对象锁
1
| 对象锁是用于对象实例方法,或者一个对象实例上的。但是需要注意的是,被锁的对象不能发生改变,更不能创建对象,因为这会导致锁失效。不同对象的锁可以同时操作同一属性。
|
类锁
1
| 类锁其实锁的是每个类对应的class 对象,类锁是用于类的静态方法或者一个类的class 对象上的
|
轻量级锁volatile的用法和使用场景
1 2 3
| volatile关键字可以保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
volatile虽然保证了操作可见性,但是不能保证变量在多线程操作下的线程安全。所以volatile的使用场景是:只有一个线程写,多个线程读的场景。
|
ThreadLocal的辨析
ThreadLocal的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class ThreadLocalTest {
static ThreadLocal<String> threadLocal = new ThreadLocal<>(); static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
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(); } }
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) { 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(); } }
|
1 2 3 4 5
| 运行结果: Thread-0:线程0 Thread-2:线程2 Thread-1:1 Thread-1:线程1
|
从结果可以看出,threadLocal在每个线程中都完成了安全的赋值,threadLocal2在线程1完成了线程安全的赋值。
究竟ThreadLocal是如何保证线程安全的呢?
先说说结论:通过每个线程使用ThreadLocal的副本数据才保证线程安全的。意思是每个线程都会拿到threadLocal的初始值,然后在自己线程中备份一个这个值,当对这个值进行操作的时候,各自线程使用各自备份的这个值,其他线程无法修改自己线程的值,所以保证了线程安全。
ThreadLocal实现解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ... }
|
从以上代码和如下ThreadLocal图解可知道,每一个线程都持有一个**ThreadLocalMap**对象,ThreadLocalMap中保存Entry对象,其中每一个Entry都包括<Key,Value>键值对,键是用户建的ThreadLocal对象,值是初始化值。然后当线程需要处理到ThreadLocal中的值时,每一个线程会将值拷贝一份到线程中进程独自操作这个值,从而实现了线程安全。
ThreadLocal引发的泄露问题
(坚持三个原则:发现问题,定位问题,解决问题)
发现问题:
运行一下代码(别释放remove注释),设置一下堆区大小,很快就会发现OOM了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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");
} }); } } }
|
定位问题:
结合下图和从3.2分析可以看到,当前线程是会持有ThreadLocalMap对象,虽然map中key持有的threadlocal对象,他是弱引用,在GC的时候会被回收,但是Value值是强应用,在GC的时候,只要线程没有运行结束,value对象不会被释放。所以在线程里一直创建对象,就会导致内存泄漏。
1 2 3 4 5 6 7 8 9
| static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
|
其实我们在测试的过程中会发现只会泄漏一部分内存,原因是什么呢?
查看ThreadLocal中方法调用栈:
get() -> replaceStaleEntry() -> expungeStaleEntry()
set() -> replaceStaleEntry() -> expungeStaleEntry()
remove() -> expungeStaleEntry()
**结论:从调用来看,get、set、remove最终都会调用到expungeStaleEntry(),expungeStaleEntry()会删除map中key为null的节点。但是每次get和set不会立马调用,所以才会导致泄漏一部分。**
解决问题:
因为remove()方法会立马调用到expungeStaleEntry()来清除key为空的过时条目。所以在使用完成之后,最好调用一下remove()方法,尽快回收不用内存空间(如上面代码屏蔽掉的代码)。
ThreadLocal的线程不安全
运行如下代码可以发现如果Number声明成**静态对象**,到导致线程不安全。因为静态对象在堆空间中只有一份,每次修改之后ThreadLocal在每个线程中备份的那份都是随线程修改这个值一直改变的,所以会存在线程不安全。正确做法:**声明放在threadlocal中的对象不能是静态的即可。**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class ThreadLocalUnsafe implements Runnable {
public Number number = new Number(0);
@Override public void run() { number.setNum(number.getNum()+1); value.set(number); 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 + "]"; } }
}
|
Synchronized和ThreadLocal的区别
ThreadLocal是一个线程隔离的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现;
synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。
虽然两个实现线程安全的手段不同,设计初衷也不同,没有可比性。
但是我还是想简单的总结一下:**synchronized实现线程安全的方案是 时间换空间的方案;而ThreadLocal实现线程安全的方式是空间换时间的方案**
测试用例代码见: git@github.com:oujie123/UnderstandingOfThread.git