线程基础阶段性总结和扩展(一)

一、前言

前面重点阐述了syschronized和volatile,这一章我们讲一下前面遗漏的一下东西。

二、死锁

  • 死锁: 一组互相竞争资源的线程因互相等待,导致永久阻塞的现象。
  • 活锁: 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试失败失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开

2.1 死锁发生的条件

这四个条件同时满足,就会产生死锁:
  • 互斥,共享资源 X Y 只能被一个线程占用;
  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

 死锁代码demo:

模拟扣款——Accout.class表示账户

public class Account {
    private String accountName;
    private int balance;

    public Account(String accountName, int balance) {
        this.accountName = accountName;
        this.balance = balance;
    }

    public void debit(int amount){ //更新转出方的余额
        this.balance-=amount;
    }

    public void credit(int amount){ //更新转入方的余额
        this.balance+=amount;
    }
}

 TransferAccountDeadLock.class

public class TransferAccountDeadLock implements  Runnable{

    private Account fromAccount; //转出账户
    private Account toAccount; //转入账户
    private int amount;

    public TransferAccountDeadLock(Account fromAccount, Account toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }

    @Override
    public void run() {
        while(true){
            synchronized (fromAccount) {
                synchronized (toAccount) {
                    if (fromAccount.getBalance() >= amount) {
                        fromAccount.debit(amount);
                        toAccount.credit(amount);
                    }
                }
            }
            //转出账户的余额
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //转入账户的余额
            System.out.println(toAccount.getAccountName() + "->" + toAccount.getBalance());
        }
    }

    public static void main(String[] args) {
        Account fromAccount=new Account("Mic",100000);
        Account toAccount=new Account("花花",300000);
        Thread a =new Thread(new TransferAccountDeadLock(fromAccount,toAccount,10));
        Thread b=new Thread(new TransferAccountDeadLock(toAccount,fromAccount,30));

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

 该代码运行一段时间后,会阻塞,表明其发生了死锁。

2.2 如何解决死锁问题

按照前面说的四个死锁的发生条件,我们只需要破坏其中一个,就可以避免死锁的产生。
其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥,其他三个条件都有办法可以破坏
  1. 对于占用且等待这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
  2. 对于不可抢占这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动 释放它占有的资源,这样不可抢占这个条件就破坏掉了。
  3. 对于循环等待这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序 的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

 2.2.1 打破“占用且等待”条件

Allocator.class,作用:同一申请锁,同一释放锁

public class Allocator {

    private List<Object> list=new ArrayList<>();
    synchronized  boolean apply(Object from,Object to){
        if(list.contains(from)||list.contains(to)){
            return false;
        }
        list.add(from);
        list.add(to);
        return true;
    }

    synchronized void free(Object from,Object to){
        list.remove(from);
        list.remove(to);
    }
}

 TransferAccount.class

package org.example;

public class TransferAccount implements  Runnable{

    private Account fromAccount; //转出账户
    private Account toAccount; //转入账户
    private int amount;
    Allocator allocator;

    public TransferAccount(Account fromAccount, Account toAccount, int amount,Allocator allocator) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
        this.allocator=allocator;
    }


    @Override
    public void run() {
        while(true){
            if(allocator.apply(fromAccount,toAccount)) {
                try {
                    synchronized (fromAccount) {
                        synchronized (toAccount) {
                            if (fromAccount.getBalance() >= amount) {
                                fromAccount.debit(amount);
                                toAccount.credit(amount);
                            }
                        }
                    }
                    //转出账户的余额
                    System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
                    //转入账户的余额
                    System.out.println(toAccount.getAccountName() + "->" + toAccount.getBalance());
                }
                finally {
                    allocator.free(fromAccount,toAccount);
                }
            }
        }
    }

    public static void main(String[] args) {
        Account fromAccount=new Account("Mic",100000);
        Account toAccount=new Account("花花",300000);
        Allocator allocator=new Allocator();
        Thread a =new Thread(new TransferAccount(fromAccount,toAccount,10,allocator));
        Thread b=new Thread(new TransferAccount(toAccount,fromAccount,30,allocator));

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

 2.2.2 打破“不可抢占”条件

通过ReentrantLock实现:

public class TransferAccount02 implements  Runnable{

    private Account fromAccount; //转出账户
    private Account toAccount; //转入账户
    private int amount;
    Lock fromLock=new ReentrantLock();
    Lock toLock=new ReentrantLock();

    public TransferAccount02(Account fromAccount, Account toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }


    @Override
    public void run() {
        while(true){
            if (fromLock.tryLock()) { //返回true和false
                if (toLock.tryLock()) {//返回true和false
                    if (fromAccount.getBalance() >= amount) {
                        fromAccount.debit(amount);
                        toAccount.credit(amount);
                    }
                }
            }
            //转出账户的余额
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //转入账户的余额
            System.out.println(toAccount.getAccountName() + "->" + toAccount.getBalance());
        }
    }

    public static void main(String[] args) {
        Account fromAccount=new Account("Mic",100000);
        Account toAccount=new Account("花花",300000);
        Thread a =new Thread(new TransferAccount02(fromAccount,toAccount,10));
        Thread b=new Thread(new TransferAccount02(toAccount,fromAccount,30));

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

2.2.3 打破“不循环等待”条件

public class TransferAccount03 implements  Runnable{

    private Account fromAccount; //转出账户
    private Account toAccount; //转入账户
    private int amount;

    public TransferAccount03(Account fromAccount, Account toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }


    @Override
    public void run() {
        Account left=null;
        Account right=null;
        if(fromAccount.hashCode()>toAccount.hashCode()){
            left=toAccount;
            right=fromAccount;
        }
        while(true){
            synchronized (left) { //返回true和false
                synchronized (right) {//返回true和false
                    if (fromAccount.getBalance() >= amount) {
                        fromAccount.debit(amount);
                        toAccount.credit(amount);
                    }
                }
            }
            //转出账户的余额
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //转入账户的余额
            System.out.println(toAccount.getAccountName() + "->" + toAccount.getBalance());
        }
    }

    public static void main(String[] args) {
        Account fromAccount=new Account("Mic",100000);
        Account toAccount=new Account("花花",300000);
        Thread a =new Thread(new TransferAccount03(fromAccount,toAccount,10));
        Thread b=new Thread(new TransferAccount03(toAccount,fromAccount,30));

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

三、Thread.join

Thread.join ,这个内容在我讲 Happens-Before 可见性模型的时候讲过,它的作用其实就是让线程的执行结果对后续线程的访问可见
换句话说,他的作用是: “等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是  在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行

 

线程是如何被阻塞的?又是通过什么方法唤醒的呢?先来看看Thread.join方法做了什么事情?
——下面我们逐步分析。

3.1 demo

public class JoinDemo {


    private static int i=10;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            i=30;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        //Callable/Future(阻塞)
        //
        t.start();
//        Thread.sleep(?); //你怎么知道多久能够执行完毕?
        //t线程中的执行结果对于main线程可见.
        t.join(); //Happens-Before模型 |
        //我希望t线程的执行结果可见
        System.out.println("i:"+i);
    }

}

3.2 join()运行图解

3.3 Thread.join()方法的实现原理

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");
    }

    // 当millis为0时,表示不限时长地等待
    if (millis == 0) {
        // 通过while()死循环,只要线程还活着,那么就等待
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 当millis不为0时,就需要进行超时时间的计算,然后让线程等待指定的时间
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
  •  join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞,wait方法的实现原理我们在后续的文章再说详细阐述。但是我们需要知道的是,调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是previousThread本身的实例。

a.之前我不理解join为什么阻塞的是主线程呢?

不理解的原因是阻塞主线程的方法是放在previousThread这个实例作用,让大家误以为应该阻塞previousThread线程。实际上主线程会持有previousThread这个对象的锁,然后调用wait方法去阻塞,而这个方法的调用者是在主线程中的。所以造成主线程阻塞。

b.第二个问题,为什么previousThread线程执行完毕就能够唤醒住线程呢?或者说是在什么时候唤醒的?

要了解这个问题,我们又得翻jdk的源码,但是如果大家对线程有一定的基本了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕以后会有一个唤醒的操作,只是我们不需要关心。

接下来在hotspot的源码中找到 thread.cpp,看看线程退出以后有没有做相关的事情来证明我们的猜想:

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
  assert(this == JavaThread::current(),  "thread consistency check");
  ...
  // Notify waiters on thread object. This has to be done after exit() is called
  // on the thread (if the thread is the last thread in a daemon ThreadGroup the
  // group should have the destroyed bit set before waiters are notified).
  ensure_join(this); 
  assert(!this->has_pending_exception(), "ensure_join should have cleared");
  ...

观察一下 ensure_join(this)这行代码上的注释,唤醒处于等待的线程对象,这个是在线程终止之后做的清理工作,这个方法的定义代码片段如下:

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
  //这里是清除native线程,这个操作会导致isAlive()方法返回false
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);//注意这里
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞的主线程会被唤醒; 到目前为止,我们基本上对join的原理做了一个比较详细的分析

总结,Thread.join其实底层是通过wait/notifyall来实现线程的通信达到线程阻塞的目的;当线程执行结束以后,会触发两个事情,第一个是设置native线程对象为null、第二个是通过notifyall方法,让等待在previousThread对象锁上的wait方法被唤醒。

3.4 什么时候会使用Thread.join

在实际应用开发中,我们很少会使用thread.join。在实际使用过程中,我们可以通过join方法来等待线程执行的结果,其实有点类似future/callable的功能。

3.3,3.4参考了这篇文章:https://www.jianshu.com/p/fc51be7e5bc0

四、面试题

sleep , join() /yiled() 的区别

sleep 让线程睡眠指定时间 , 会释放 cpu 时间片
join wait/notify , 让线程的执行结果可见
yiled 让出时间片 . -> 触发重新调度 .
sleep(0) -> 触发一次切换
Java 中能够创建 volatile 数组吗?
可以创建 , Volatile 对于引用可见,对于数组中的元素不具备可见性。
// volatile 缓存行的填充 . -> 性能问题
Java 中的 ++ 操作是线程安全的吗?
不是线程安全的, 原子性、有序性、可见性。
++ 操作无法满足原子性
线程什么时候会抛出 InterruptedException()
t.interrupt() 去中断一个处于阻塞状态下的线程时( join/sleep/wait
Java Runnable Callable 有什么区别
T1/T2/T3 三个线程,如何确保他们的执行顺序
join
Java 内存模型是什么?
JMM 是一个抽象的内存模型。
它定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储到内存以及从内
存中取出共享变量的底层实现细节。通过这些规则来规范对内存的读写操作从而保证指令的正确
性,它解决了 CPU 多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的
可见性。
什么是线程安全
原子性、有序性、可见性(硬件层面( CPU 高速缓存、指令重排序、 JMM ))

五、ThreadLocal

单开一篇,详情请见:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值