线程之间的共享与协作

线程之间的共享与协作

共享

一个进程可以有多个线程,这些线程共享进程分配到的资源。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错误案例结果
可见,在synchronized的修饰下,按理来说,5个线程输出的i应为2,3,4,5,6五个结果,但实际并非如此。
这种情况下,我们可以判定synchronized加锁的对象出了错,虚拟机中对象有自己存放的地址,故对i的地址加以输出,排查原因。这里用到System.identityHashCode(i)
synchronized错误案例结果-打印地址版
可以看到,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修饰符时的调用结果:
加volatile的结果
对ready不加volatile修饰符时的调用结果:
不加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();
        }
    }
}

使用ThreadLocal
实现
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);
        }
        
}

大致可以总结如下:
ThreadLocal解析
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()实现一个等待超时模式的连接池

等待超时:在指定时间内获得结果则返回结果,反之则抛出异常或者返回默认结果。

  1. 连接池–需要一个池的容器(变量),存放连接
LinkedList<Connection> pool = new LinkedList<Connection>();
  1. 池中存放连接数-最大支持连接数
    在构造方法中创建指定size的连接,并放入pool中。

  2. 获取/释放连接
    定义两个方法,一个从池中获取连接,一个将获取到的连接放回池中。
    ①获取连接:以下三种情况可能:
    Ⅰ. 正常获得连接
    Ⅱ.池中无连接,等待中
    Ⅲ.超时
    即,从池中拿连接前,需要判断1.池中是否有连接,2.是否超时;当池中无连接,但未超时时,需要wait(),直至池中有连接,或者超时。
    ②释放连接:将连接放回pool中,同时通知(notifyAll)其他等待连接的线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值