线程之间的共享与协作
- 线程之间的共享与协作
- 共享
- synchronized
- 错误加锁
- volatile
- 协作
- ThreadLocal
- wait()、notify()/notifyAll()
- 使用wait()/notifyAll()实现一个等待超时模式的连接池
线程之间的共享与协作
共享
一个进程可以有多个线程,这些线程共享进程分配到的资源。Java中支持多个线程同时访问一个对象或者对象的成员变量,也就是线程之间的共享。而当多个线程同时访问同一个对象或者同一个变量时,就可能发生线程安全问题,因此Java提供了synchronized及volatile保证线程共享的安全问题。
synchronized
synchronized是JVM提供的内置锁,有两种种使用形式,如下:
private long count =0;
private Object obj = new Object();
/*1.用在同步块上*/
public void sum1(){
synchronized (obj){
count++;
}
}
/*2.用在方法上
本质:public synchronized(this) void sum2()*/
public synchronized void sum2(){
count++;
}
/*3. 用在同步块上,但是锁的是当前类的对象实例*/
public void sum3(){
synchronized (this){
count++;
}
}
同步方法与同步块,本质都是给某个对象加锁。
除了上述使用方式外,synchronized还可以加在静态方法上。
private static synchronized void synStatic1(){
System.out.println(Thread.currentThread().getName() +"synStatic1 start...");
SleepTools.second(1);
System.out.println(Thread.currentThread().getName() +"synStatic1 end...");
}
private static Object obj = new Object();
private static void synStatic2(){
synchronized (obj){
System.out.println(Thread.currentThread().getName() +"synStatic2 start...");
SleepTools.second(1);
System.out.println(Thread.currentThread().getName() +"synStatic2 end...");
}
}
为什么可以在static方法上加锁
static方法上的锁,其加锁对象是虚拟机中的class对象。本质仍是对象锁,但类锁与对象锁之间互不干扰。但加在static对象上的锁,加锁对象时这个静态对象,而不是class对象,所以synStatic1()与synStatic2()可以并行。
总结
synchronized实质是对某个对象加锁,也就是对象锁,同步块可以明显看到加锁的对象,同步方法的形式是synchronized修饰在方法上,其加锁对象实际是this,方法所在类的实例。用synchronized修饰静态方法加上的锁又称类锁,加锁的对象是clas对象。
错误加锁
有这么一个应用场景:
public class TestSynWrong {
public static void main(String[] args) {
Worker worker=new Worker(1);
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();
i++; System.out.println(thread.getName()+"--"+i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
可见,在synchronized的修饰下,按理来说,5个线程输出的i应为2,3,4,5,6五个结果,但实际并非如此。
这种情况下,我们可以判定synchronized加锁的对象出了错,虚拟机中对象有自己存放的地址,故对i的地址加以输出,排查原因。这里用到System.identityHashCode(i)。
可以看到,Thread_0,2,4加锁的i地址各不相同,Thread_1,3同地址但并行执行。查看反编译(JD-GUI)的class文件后可以发现i++实际是返回了一个新的Integer对象,也就是说在i++之后,i指向的对象发生了变化,但当前锁的对象仍是原来所指向的地址中存放的对象,而对于其他线程而言,可视为当前线程已释放了i对象的锁,可以执行。
Thread1,3获得i的地址相同并行执行
猜测:虽然此时i对应的地址相同,但实际锁的对象不是同一个。
在1,3前0,2,4三个线程分别释放了3个对象–即使当前i不指向这三个对象中的任意一个,但仍是持有i锁的对象,对于1,3线程来说,此刻有3把锁可以抢,在抢到锁执行内容时,又是获取的i当前指向的对象进行运算,故出现类似于抢了同一把锁并行执行的假象。
System.identityHashCode(i)
/**
* Returns the same hash code for the given object as
* would be returned by the default method hashCode(),
* whether or not the given object's class overrides
* hashCode().
* The hash code for the null reference is zero.
*
* @param x object for which the hashCode is to be calculated
* @return the hashCode
* @since JDK1.1
*/
public static native int identityHashCode(Object x);
注释翻译:identityHashCode()为给定对象返回与默认方法hashCode()返回的哈希码相同的哈希码,无论给定对象的类是否覆盖 hashCode()。 空引用的哈希码为零。
volatile
jdk中称为最轻量的同步机制,也就是说volatile保证一个线程对修饰变量的修改对其他线程立即可见–可见性,但不保证多个线程同时修改时的线程安全。
public class TestVolatile {
// private static boolean ready;
private volatile static boolean ready;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready) { //无限循环,number打印不会进行
;
}
System.out.println("PrintThread end....");
}
}
public static void main(String[] args) {
new PrintThread().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("modify ready to true..");
ready = true; //线程并没有感知到对ready的修改---ready新值不可见
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main is ended!");
}
}
对ready加上volatile修饰符时的调用结果:
对ready不加volatile修饰符时的调用结果:
最常见的应用场景:一写多读,一个线程写,多个线程读。加上volatile的变量,在一个线程中修改的时候,其他多个线程可以立即读到该修改值。
为什么不可见jdk内部实现,Java内存模型等等
协作
ThreadLocal
保证并发访问,为每个线程提供一个变量的副本,实现线程隔离。Spring事务中,保证每个线程自己的连接。@Transational–在方法的前后插入事务的connection/commit。
使用
public class ThreadLocalUse {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
//初始化值
@Override
protected Integer initialValue() {
return 1;
}
};
// private static Integer count = 0;
public static class Thread1 extends Thread{
private Integer num;
public Thread1(Integer num){
this.num = num;
}
@Override
public void run(){
Integer value = threadLocal.get();
// count += num;
value+=num;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName()+": "+ threadLocal.get());
// System.out.println(Thread.currentThread().getName()+": "+ count);
}
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
Thread1 thread = new Thread1(i);
thread.start();
}
}
}
实现
get()
get()方法首先获得当前线程,之后获取线程的变量threadLocals【ThreadLocal.ThreadLocalMap】,根据当前ThreadLocal为key获取对应的value值。
public class ThreadLocal<T> {
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();
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
在Thread的源码中可以看到threadLocals是一个Map,
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
从ThreadLocal的源码找到ThreadLocalMap部分:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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()
set()方法第一步也是获取当前线程,从而拿到对应线程的ThreadLocalMap。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap的set()方法,
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
引发的内存泄漏问题
ThreadLocalMap中Entry对ThreadLocal是WeakReference 弱引用关系,而弱引用的对象在JVM垃圾回收时必会被回收,若当前线程想要获取该ThreadLocal的值时,通过Map的getEntry(),此时key(ThreadLocal)为null,获取不到其value,就会导致内存泄漏。
ThreadLocal线程不安全
原因:Map中存放的是对象的引用。当多个线程中共享一个ThreadLocal时,若ThreadLocal对象被修改就会导致线程不安全的问题。
wait()、notify()/notifyAll()
wait()是等待,notify()/notifyAll()是通知,必须在synchronized中调用,这三个方法在Object类中。
标准范式
wait:
synchronized(obj){
while(不满足条件){
obj.wait();//释放锁
}
//业务逻辑
}
notify:
synchronized(obj){ //等待锁
//改变条件,业务
obj.notifyAll();//obj.notify();
...//执行完才会释放锁
}
wait释放占有的锁,notify唤醒wait所在线程,进入就绪状态,等待notify所在线程执行完毕释放锁后争抢锁。notify只会任意唤醒一个等待中的线程,而notifyAll会唤醒所有wait的线程。
使用wait()/notifyAll()实现一个等待超时模式的连接池
等待超时:在指定时间内获得结果则返回结果,反之则抛出异常或者返回默认结果。
- 连接池–需要一个池的容器(变量),存放连接
LinkedList<Connection> pool = new LinkedList<Connection>();
- 池中存放连接数-最大支持连接数
在构造方法中创建指定size的连接,并放入pool中。 - 获取/释放连接
定义两个方法,一个从池中获取连接,一个将获取到的连接放回池中。
①获取连接:以下三种情况可能:
Ⅰ. 正常获得连接
Ⅱ.池中无连接,等待中
Ⅲ.超时
即,从池中拿连接前,需要判断1.池中是否有连接,2.是否超时;当池中无连接,但未超时时,需要wait(),直至池中有连接,或者超时。
②释放连接:将连接放回pool中,同时通知(notifyAll)其他等待连接的线程。