并发编程(4):死锁、Thread.join()原理、ThreadLocal原理

1、死锁,什么是死锁?

      死锁就是存在多个共享资源的时候,存在多线程环境对共享资源的访问的时候,出现了线程之前彼此等待对方释放各自所需要的锁,这就是死锁的概念。

       1.1、案例:

               我们使用转账来说明死锁,定义两个账户,使用两条线程来进行相互转账。

               a 线程由 acountW ------>  acountZ

               b 线程反过来acountZ -------> acountW

               Acount代码:

public class Acount {
    private String name;
    private Integer money;

    public Acount(String name, Integer money) {
        this.name = name;
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    //转入 即加钱
    public void transInfo(Integer currentMoney){
        this.money = money + currentMoney;
    }

    //转出 即减钱
    public void transOut(Integer currentMoney){
        this.money = money - currentMoney;
    }
}

              转账服务TransferAccounts:

public class TransferAccounts implements Runnable {
    private Acount acountForm;
    private Acount acountTo;
    private Integer currentMoney;


    public TransferAccounts(Acount acountForm, Acount acountTo, Integer currentMoney) {
        this.acountForm = acountForm;
        this.acountTo = acountTo;
        this.currentMoney = currentMoney;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (acountForm) {
                synchronized (acountTo) {
                    acountForm.transOut(currentMoney);
                    acountTo.transInfo(currentMoney);
                }
            }
            System.out.println(acountForm.getName() + "--转出:" + currentMoney + ", 当前余额:" + acountForm.getMoney());
            System.out.println(acountTo.getName() + "--转入:" + currentMoney + ", 当前余额:" + acountTo.getMoney());
        }
    }
}

                 测试代码Client: 

    public static void main(String[] args) {
        Acount acountW = new Acount("W", 300000);
        Acount acountZ= new Acount("Z", 10);

        //W---》Z
        TransferAccounts transferAccountsWtoZ = new TransferAccounts(acountW, acountZ, 100);

        //Z---》W
        TransferAccounts transferAccountsZtoW = new TransferAccounts(acountZ, acountW, 10);

        Thread  a = new Thread(transferAccountsWtoZ);
        Thread  b= new Thread(transferAccountsZtoW);

        a.start();
        b.start();

        //以上运行会出现死锁,两条线程可能会出现:
        //             a 线程锁住了 acountW 账户  等待 b 线程释放  acountZ 锁
        //             b 线程锁住了 acountZ  账户  等待 b 线程释放 acountW 锁
        //             这就造成了两条线程彼此等待对方释放锁需要的锁,因此造成了死锁。

    }

                  发生死锁展示:

                          

          1.2、发生死锁的条件:如下4个条件同时满足就会造成死锁。

                   1、存在互斥性:即共享资源 X 和 Y 只能被一个线程占用; 

                   2、占有且等待:即线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; 

                   3、不可抢占性:其他线程不能强行抢占线程 T1 占有的资源;

                   4、存在循环等待:线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 

 

2、如何解决死锁?

       解决死锁的方式主要就是围绕着破坏死锁成立的4个条件来实现。

       2.1、破坏存在互斥性我么可以使用一把锁来锁定两个共享资源在同一时刻只能被一条线程访问,如下我们使用ReentrantLock来实现。

/**
 * 使用ReentrantLock来解决账户转账死锁问题
 */
public class SolveMode1TransferAccount implements Runnable {
    private Acount acountForm;
    private Acount acountTo;
    private Integer currentMoney;

    static ReentrantLock lock = new ReentrantLock();

    public SolveMode1TransferAccount(Acount acountForm, Acount acountTo, Integer currentMoney) {
        this.acountForm = acountForm;
        this.acountTo = acountTo;
        this.currentMoney = currentMoney;
    }

    @Override
    public void run() {
        while (true) {
            //使用一把ReentrantLock 来解决死锁
            if (lock.tryLock()) {
                acountForm.transOut(currentMoney);
                acountTo.transInfo(currentMoney);
                System.out.println(acountForm.getName() + "--转出:" + currentMoney + ", 当前余额:" + acountForm.getMoney());
                System.out.println(acountTo.getName() + "--转入:" + currentMoney + ", 当前余额:" + acountTo.getMoney());
            }
        }
    }
}

        2.2、对于占有且等待 我们也可以使用ReentratLock 来进行破坏,本案例修改如下:

/**
 * 使用ReentrantLock来解决账户转账死锁问题
 */
public class SolveMode1TransferAccount implements Runnable {
    private Acount acountForm;
    private Acount acountTo;
    private Integer currentMoney;

    ReentrantLock lockFrom;
    ReentrantLock lockTo;

    public SolveMode1TransferAccount(Acount acountForm, Acount acountTo, Integer currentMoney, ReentrantLock lockFrom, ReentrantLock lockTo) {
        this.acountForm = acountForm;
        this.acountTo = acountTo;
        this.currentMoney = currentMoney;
        this.lockFrom = lockFrom;
        this.lockTo = lockTo;
    }

    @Override
    public void run() {
        while (true) {
            //使用两把ReentrantLock 来解决死锁,因为tryLock()方法是尝试获取锁,不会阻塞,会立马返回获取锁结果,
            // 因此当一个线程获取到了一把锁的时候,当去申请另外一把锁的时候,如果这把锁被其他线程占有那么tryLock()
            //方法将会返回false,那么在本案例中将会进入下一次循环,
            if (lockFrom.tryLock()) {
                if (lockTo.tryLock()) {
                    acountForm.transOut(currentMoney);
                    acountTo.transInfo(currentMoney);
                    System.out.println(acountForm.getName() + "--转出:" + currentMoney + ", 当前余额:" + acountForm.getMoney());
                    System.out.println(acountTo.getName() + "--转入:" + currentMoney + ", 当前余额:" + acountTo.getMoney());
                }
            }
        }
    }

        2.3、对于不可抢占性我们一般是没办法进行破坏的。

        2.4、我们可以破坏存在循环等待 这个条件,那就是定义线程获取锁的方向是一致的。

                 比如: a 线程在转账的时候是先获取W锁,然后才获取Z锁。那我们也让b线程也是先获取W锁,然后才获取Z锁。这种方式在本案例中与业务逻辑会有违背,但是这样确实是可以去破坏死锁的存在循环等待这个条件的。

 

3、Thread.join() 原理

      3.1、Thread.join() 基本使用方式:

public class ThreadJoinDemo implements Runnable{
    @Override
    public void run() {
        System.out.println("thread join test...");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadJoinDemo());
        thread.start();
        //让thread 线程先执行
        thread.join();
        System.out.println("end...");
    }
}

                输出结果:从输出结果可以看出在main线程中调用thread 线程的join()方法,那么main线程就会等待thread线程执行完成后才会继续执行。 这个就是Thread.join()方法的作用。

                 

      3.2、Thread.join()的原理:

                 从表现出来的形式来看,应该是执行执行thread.join()方法的线程(案例中的主线程)发生了阻塞,然后早thread执行完成后由唤醒的阻塞的线程(案例中的的主线程)。这种模式在我们前面讲的线程通讯(wait/notify/notifyAll)是一个道理,那么Thread.join()的原理到底是不是wait/notify/notifyAll 呢?接下来我们查看其源码来解释。

                Thread.join(long)方法源码:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                //阻塞当前线程,而我们是使用main线程来执行此方法的,因此阻塞的就是main线程。
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                //阻塞当前线程,而我们是使用main线程来执行此方法的,因此阻塞的就是main线程。
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

                   从源码显示来看,确实是通过wait()方法阻塞的父线程(案例中的main线程),我们都知道,在调用wait的时候需要使用一个Object实例,那么在Thread.join()方法中的实例是啥呢,没错就是子线程实例(案例中的thread实例)。在子线程终止的时候,会去唤醒所有使用此子线程实例进行wait阻塞的线程。

                   从上面的分析来看,在子线程终止后会唤醒父线程(案例中的main线程),那么是如何实现的呢?在Hotspot源码的thread.cpp中有如下代码:

                  在 JavaThread::exit方法中会调用ensure_join(this) 其中this为当前终止的线程实例。

                  ensure_join(this)方法Hotspot源码:会唤醒使用当前实例进行wait的其他线程(比如案例中的main线程)。      

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  java_lang_Thread::set_thread(threadObj(), NULL);

  //在此处唤醒被当前线程实例阻塞的其他线程,如案例中的main线程。
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

 

4、ThreadLocal 原理

      4.1、什么是ThreadLocal ?

               ThreadLocal就是本地线程变量,就是把一个变量跟一个线程做绑定,然后我们使用当前线程可以获取到绑定的变量,也可以理解为线程数据隔离。通俗的说法就是某数据属于当前线程。

      4.2、ThreadLocal 的简单使用:

public class ThreadLocalDemo01 {

    public static ThreadLocal<App> appThreadLocal  = ThreadLocal.withInitial(() -> new App("default"));

    public static void main(String[] args) {
        for (int i= 0; i<5 ;i++){
            new Thread(() -> {
                //从appThreadLocal中获取到一个初始值 即一个 new App("default"),每个线程都会给一个。
                App currentApp = appThreadLocal.get();
                currentApp.setName(Thread.currentThread().getName());

                //设置名称后添加到当前线程的本地变量中
                appThreadLocal.set(currentApp);

                //从appThreadLocal中获取当前的线程本地变量
                System.out.println(Thread.currentThread().getName() + "--" + appThreadLocal.get()+"   " + appThreadLocal.get().getName() + "");
            },"Thread-- "+ i).start();
        }
    }
}

               分析输出:

Thread-- 0--com.wzy.threadstudy.A07threadLocal.App@2181a0f3   Thread-- 0
Thread-- 4--com.wzy.threadstudy.A07threadLocal.App@7be6bb31   Thread-- 4
Thread-- 1--com.wzy.threadstudy.A07threadLocal.App@3b7fe55f   Thread-- 1
Thread-- 3--com.wzy.threadstudy.A07threadLocal.App@3d3dbc1b   Thread-- 3
Thread-- 2--com.wzy.threadstudy.A07threadLocal.App@109ec86   Thread-- 2

               从输出上可以看出,每个线程都有自己的一个本地变量,跟其他线程的完全隔离。

 

               注意事项:一定是每个线程使用自己的变量,如果是共享资源依旧无法实现线程隔离,如下:

public class ThreadLocalDemo02 {

    public static App app = new App("default");

    //在获取初始值的时候,返回的是一个静态变量--->共享资源
    public static ThreadLocal<App> local  = ThreadLocal.withInitial(() -> app);

    public static void main(String[] args) throws InterruptedException {
        //1、先创建一个线程,获取到初始值 即共享实例app,将其设置到此线程的本地变量中。
        new Thread(()->{
            App app1 = local.get();
            local.set(app1);
            System.out.println("第一条线程:" + local.get().getName());
            
            //每隔一秒钟进行一次读取输出。
            while (true){
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("第一条线程:" + local.get().getName());
            }
        }).start();

        //启动5条线程进行,进行获取初始值即共享实例app,设置每一条线程的本地办变量中
        for (int i= 0; i<5 ;i++){
            new Thread(() -> {
                App currentApp = local.get();
                currentApp.setName(Thread.currentThread().getName());
                local.set(currentApp);
                System.out.println(Thread.currentThread().getName() + "--" + local.get()+"   " + local.get().getName() + "");
            },"Thread-- "+ i).start();
        }
    }
}

               分析输出:

第一条线程:default
Thread-- 0--com.wzy.threadstudy.A07threadLocal.App@301562db   Thread-- 0
Thread-- 3--com.wzy.threadstudy.A07threadLocal.App@301562db   Thread-- 3
Thread-- 1--com.wzy.threadstudy.A07threadLocal.App@301562db   Thread-- 1
Thread-- 2--com.wzy.threadstudy.A07threadLocal.App@301562db   Thread-- 2
Thread-- 4--com.wzy.threadstudy.A07threadLocal.App@301562db   Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4
第一条线程:Thread-- 4

               从输出可以看出,如果每个线程里放的都是共享资源变量,一旦有线程进行修改,那么当前线程里的本地变量也会被修改。无法做到隔离。

 

     4.3、ThreadLocalMap 数据结构:

             在当前的线程实例中有一个ThreadLocal.ThreadLocalMap threadLocals即线程的成员变量;

             在ThreadLocalMap 中有一个Entry[] 数组默认长度为16

                   Entry实例的  key = ThreadLocal实例,  比如 使用案例中的appThreadLocal实例。

                   Entry实例的  value = 我们设置的value值比如 0。

 

             entry实例数据index的确定方式:

                   使用ThreadLocal 实例计算hashCode  & (16 -1)。

 

             一个线程可以使用多个ThreadLocal 实例来设置多个需要的本地线程变量。

 

             ThreadLocal 数据流转图:

                 

             ThreadLocal.set(T value)的源码分析:

    //使用ThreadLocal实例来设置当前线程的本地变量
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();

        //获取当前线程的ThreadLocalMap 实例, return t.threadLocals;
        ThreadLocalMap map = getMap(t);

        
        if (map != null)
            //如果当前线程的ThreadLocalMap 实例不为=null,那就调用ThreadLocalMap 实例的set方法
            map.set(this, value);
        else
            //如果如果当前线程的ThreadLocalMap 实例为空,就为当前创建一个ThreadLocalMap实例,并设置当前的值
            createMap(t, value);
    }
    
    // createMap(t, value) 方法
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

     
    //new ThreadLocalMap(this, firstValue), INITIAL_CAPACITY = 16
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
    }

           ThreadLocal.ThreadLocalMap.set(ThreadLocal<?> key, Object value)源码分析:

         private void set(ThreadLocal<?> key, Object value) {

            //获取到当前线程的theadLocalMap实例中的Entry[]。
            Entry[] tab = table;

            //获取到当前线程的theadLocalMap实例中的Entry[]数组的长度。
            int len = tab.length;

            //计算当前ThreadLocal实例需要放在的Entry[]的下标。
            int i = key.threadLocalHashCode & (len-1);

            /*
              以计算出来的下标i向后遍历Entry[]数组。如果当前i的entry不为空,
              那么说明hash冲突了,冲突后解决hash冲突的发方式就是线性探索,方
              式就是数组向后探索。
              
              在向后遍历的时候也同时将之前计算的下标i重新赋值给当次循环,也就
              是说出现hash冲突后,会向数组后面逐个寻找,寻找到一个合适的下标。
              
            */
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                
                //如果当前遍历的entry实例的key=当前ThreadLocal的实例,那就替换值为本次设置的值
                if (k == key) {
                    e.value = value;
                    return;
                }

                /*如果变遍历的entry实例的key=null,entry 实例不为空,但是entry实例的 
                   key=null,由于key是弱应用,如果设置key表示的ThreadLocal实例=null,
                   那么就会被jvm回收,因此会出现key=null的现象。出现这种脏entry后需要
                   进行处理,处理的方式还是线性探索,但是此时会数组前后进行探索。
                */
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            
            //如果当前数组中此下标的entry为空,那就是用当前的ThreadLocal实例为key,value为需要设置的值创建一个entry实例并添加到entry数组的i下标中。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

                       

               向后线性探测解决hash冲突 + 处理脏entry(双向线性探测解决脏entry问题):

                  获取本地线程变量源码分析:

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();

        //获取到当前线程的ThreadLocalMap 实例
        ThreadLocalMap map = getMap(t);

        if (map != null) {
            //如果当前获取到了当前线程的ThreadLocalMap 实例,那就使用当前的ThreadLocal实例进行hash计算确定Entry[]下标,然后通过下标获取entry实例。
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")

                //如果找到了entry 实例,就获取其value值返回。
                T result = (T)e.value;
                return result;
            }
        }
 
        //如果没有获取到了当前线程的ThreadLocalMap实例,其实就是调用initialValue()方法获取到本地变量的初始值,然后调用跟set()方法一样的逻辑,创建ThreadLocalMap实例,设置当前的初始值,然后返回初始值。
        return setInitialValue();
    }

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值