一、前言
前面重点阐述了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 如何解决死锁问题
- 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
- 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动 释放它占有的资源,这样不可抢占这个条件就破坏掉了。
- 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序 的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
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方法做了什么事情?
——下面我们逐步分析。
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
单开一篇,详情请见: