难以理解的锁(二) Threads


一、interrupt 中断

关于中断的方法

Java中线程的中断(interrupt)是指线程的中断状态,true 或 false。Interrupt相关的方法:

isInterrupted :判断线程的中断状态,如果线程被中断返回true。
interrupt :中断当前线程,将中断状态设置为true。
interrupted :静态方法,判断当前线程是否中断,并且重置中断状态为false。

设置了线程中断状态为true后,线程会自己处理这个状态。

InterruptException

Thread.sleep()、Thread.wait() 等方法声明都会有 throws InterruptException,通常称这些方法为阻塞方法。阻塞方法通常需要较长的时间返回并且依赖其他外部条件,所以如果需要快速返回的话可以使用中断实现。

ReentrantLock 中 lock 方法处理中断:
AbstractQueuedSynchronizer#acquire | acquireQueued | parkAndCheckInterrupt | selfInterrupt

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果返回true表示在等待中被中断,调用方法再次设置中断状态为true
            selfInterrupt();
    }
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果线程中断状态,设置interrupted变量为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    private final boolean parkAndCheckInterrupt() {
    	// 阻塞线程
        LockSupport.park(this);
        // 判断线程中断状态,并重新设置中断状态为false
        return Thread.interrupted();
    }
    static void selfInterrupt() {
    	// 中断当前线程(设置中断状态true)
        Thread.currentThread().interrupt();
    }

ReentrantLock 中 lockInterruptibly 方法处理中断:

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        // 如果线程状态为中断,则直接抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

可以参考这两种方式处理线程中断,达到停止线程的目的。

自动感知中断

Thread类的 join 和 sleep 的重载方法,Object类的 wait 重载方法可以自动感知到线程的中断。这几个方法声明都有 throws InterruptException,当线程阻塞在这些方法上时,如果其他线程对这个线程进行中断,此线程会从这些方法立即返回,抛出异常,并且设置中断状态为false。

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                try {
                    System.out.println("t1 acquire lock");
                    obj.wait(100000);
                    //LockSupport.park();
                    System.out.println("t1 finish park/wait");
                } catch (Exception e) {
                	System.out.println("t1 state: " + Thread.currentThread().isInterrupted());
                    System.out.println("t1 InterruptedException");
                }
            }
        });
        t1.start();
        Thread.sleep(100); // 先让线程t1获取到锁

        boolean before = t1.isInterrupted();
        t1.interrupt();
        boolean after = t1.isInterrupted();
        System.out.println("t1 interrupt before state: " + before);
        System.out.println("t1 interrupt after  state: " + after);

控制台输出(实际打印顺序每次都不一样):

t1 acquire lock
t1 state: false
t1 InterruptedException
t1 interrupt before state: false
t1 interrupt after state: true

线程t1被中断后线程的中断状态为true(并不是一定),wait方法返回,抛出异常。并且catch块中打印t1状态为false,说明中断状态又设置为false。如果在主线程中中断t1后过一段时间查看t1状态,那一定是false。
ReentrantLock 中 LockSupport.park(this) 将同步队列中线程阻塞,如果其他线程中断此线程,则线程会唤醒,但是中断状态还是true。

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                try {
                    System.out.println("t1 acquire lock");
                    //obj.wait(1000);
                    LockSupport.park();
                    System.out.println("t1 state: " + Thread.currentThread().isInterrupted());
                    System.out.println("t1 finish park/wait");
                } catch (Exception e) {
                    System.out.println("t1 InterruptedException");
                }
            }
        });
        t1.start();
        Thread.sleep(100);

        boolean before = t1.isInterrupted();
        t1.interrupt();
        boolean after = t1.isInterrupted();
        System.out.println("t1 interrupt before state: " + before);
        System.out.println("t1 interrupt after  state: " + after);

控制台输出(实际打印顺序每次都不一样):

t1 acquire lock
t1 state: true
t1 finish park/wait
t1 interrupt before state: false
t1 interrupt after state: true

二、wait 等待

java中每个对象都关联一个 monitor,使用 synchronized 加锁,实际是获取对象关联的 monitor 锁的过程。同一时间只有一个线程获取到对象的monitor锁,如果其他线程在锁占用期间获取锁,将会阻塞在队列中(重量级锁)。
在这里插入图片描述
未获取到锁的线程被封装成ObjectWaiter添加到ContenttionList(cxq)中,持有锁的线程解锁前会将ContentionList中元素移动到EntiryList中。Owner表示当前持有锁的线程,加锁的对象调用wait方法将加入WaitSet(等待集合),当被唤醒或者等待时间到期后会重新加入到EntityList,重新参与锁的竞争。

wait 方法执行情况:
1 如果 线程t 未获取到 对象o 的锁,将抛出 IllegalMonitorStateException 异常
2 如果调用wait的重载方法,timeout不能为负数,nanos范围[0,999999],否则抛出IllegalArgumentException异常
3 如果线程t被中断,则抛出InterruptedException异常,并设置线程中断状态为false
4 线程加入等待集合,并完全释放对象o上的锁(发生重入)。等待直到从等待集合中移出,移出后参与锁竞争,继续完成加锁等操作。如果线程因为中断从等待集合中移出,中断状态将设置为false,并且抛出中断异常。

线程T由于主线程的打断,退出等待,与其他100个线程参与锁竞争:

        CountDownLatch cdl = new CountDownLatch(100);
        CountDownLatch cdl_0 = new CountDownLatch(1);

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                try {
                    System.out.println("t-T acquire lock");
                    obj.wait(10000);
                } catch (Exception e) {
                    System.out.println("t-T InterruptedException");
                }
                System.out.println("t-T again acquire lock");
            }
        });
        t1.start();
        Thread.sleep(300);

        IntStream.range(0, 100).forEach(i -> new Thread(() -> {
            try {
                cdl_0.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + " acquire lock");
            }
            cdl.countDown();
        }, "t-" + i).start());

        cdl_0.countDown();
        t1.interrupt();
        cdl.await();
        System.out.println("end");

什么情况线程t会从等待集合(加锁对象对应monitor中的等待集合,是属于某个对象的)中移出,图中的d:
a 线程t被其他线程中断
b 其他线程中调用加锁对象的notify方法,有可能唤醒线程t(需要被选中)
c 其他线程中调用加锁对象的notifyAll方法,唤醒等待集合中所有线程,包括线程t
d 线程t调用wait有参数的重载方法,在指定的时间后从等待集合中移出
e JVM的“假唤醒”。

wait方法应该写在while循环中,等待某个条件成立后退出循环,避免假唤醒。

从等待集合中移出后的线程,需要等待当前线程释放锁之后,才会重新参与锁的竞争,竞争到锁才会继续执行(无论是中断抛出异常还是通知后正常执行,都需要再次获取到锁)。

三、notify、notifyAll 通知

notify、notifyAll 执行情况:
1 在 线程t对象o 调用notify或者notifyAll方法后,如果线程t 还未 获取到 对象o 的锁,抛出IllegalMonitorStateException
2 notify 方法会从等待集合中选中一个线程(如果有)移出等待集合,这个线程将在t释放锁之后参与锁竞争
3 notifyAll 方法会将等待集合中所有线程移出,这些线程参与竞争

四、sleep、yield 休眠和让步

sleep方法使当前线程停止执行指令一段时间,与wait不同的是并不会释放已经获取的锁。
yield方法告诉当前系统的调度器让出cpu给其他线程用,调度器可以不理会。

五、线程的状态

一个线程在同一时间智能处于以下状态(虚拟机状态,和操作系统没有关系)之一:NEW、RUNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

下面是源码中的注释:
NEW:尚未启动的线程的线程状态。
RUNNABLE:可运行线程的线程状态。处于runnable 状态的线程正在Java虚拟机中执行,但是它可能在等待操作系统中的其他资源,比如处理器。
BLOCKED:等待监视器锁的阻塞线程的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法,或者在调用Object.wait后重新进入同步块/方法。
WAITING:等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:Object.wait \ Thread.join \ LockSupport.park。处于等待状态的线程正在等待另一个线程执行特定的操作。例如,一个在对象上调用object.wait()的线程正在等待另一个线程在该对象上调用object. notify()或object. notifyall()。调用thread.join()的线程正在等待指定的线程终止。
TIMED_WAITING:具有指定等待时间的等待线程的线程状态。线程由于调用下列方法之一,并指定了正等待时间,因此处于定时等待状态:Thread.sleep(long) \ Object.wait(long) \ Thread.join(long) \ LockSupport.parkNanos \ LockSupport.parkUntil 。
TERMINATED:终止线程的线程状态。线程已经完成执行。

结合注释我总结了线程状态之间的转换关系(20190911更新):
在这里插入图片描述
在 WAITING 和 TIMED_WAITING 状态时,其他线程执行等待超时、notify、notifyAll、interrupt 会变为 BLOCKED 状态,竞争到锁之后才是RUNNABLE状态。(结合monitor结构那张图就比较好理解了,waitSet集合中的线程会进入EntryList集合,参与下次锁竞争)

假设有5个线程去获取 obj 锁,然后 wait 100ms,可以查看 wait 超时之后每个线程的状态:

        List<Thread> threadList = new ArrayList<>();
        Thread thread;
        for (int i = 0; i < 5; i++) {
            thread = new Thread(() -> {
                synchronized (obj) {
                    String threadName = Thread.currentThread().getName();
                    try {
                        System.out.println(threadName + " acquire lock");
                        obj.wait(100);
                        for (int j = 0; j < 5; j++) {
                            System.out.println(threadName + " -> " 
                                    + threadList.get(j).getName() + " " 
                                    + threadList.get(j).getState().name());
                        }
                        //obj.notifyAll();
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                        System.out.println(threadName +" InterruptedException");
                    }
                }
            }, "t-" + i);
            threadList.add(thread);
        }
        for (int i = 0; i < 5; i++) {
            threadList.get(i).start();
        }
        System.out.println("main thread end");

main thread end
t-0 acquire lock
t-3 acquire lock
t-2 acquire lock
t-4 acquire lock
t-1 acquire lock
t-2 -> t-0 TIMED_WAITING
t-2 -> t-1 BLOCKED
t-2 -> t-2 RUNNABLE
t-2 -> t-3 BLOCKED
t-2 -> t-4 BLOCKED
t-0 -> t-0 RUNNABLE
t-0 -> t-1 BLOCKED
t-0 -> t-2 RUNNABLE
t-0 -> t-3 BLOCKED
t-0 -> t-4 BLOCKED
t-3 -> t-0 TERMINATED
t-3 -> t-1 BLOCKED
t-3 -> t-2 TERMINATED
t-3 -> t-3 RUNNABLE
t-3 -> t-4 BLOCKED
t-1 -> t-0 TERMINATED
t-1 -> t-1 RUNNABLE
t-1 -> t-2 TERMINATED
t-1 -> t-3 TERMINATED
t-1 -> t-4 BLOCKED
t-4 -> t-0 TERMINATED
t-4 -> t-1 TERMINATED
t-4 -> t-2 TERMINATED
t-4 -> t-3 TERMINATED
t-4 -> t-4 RUNNABLE

可以看到每个线程依次获取到了锁(wait方法会释放锁),然后进入TIMED_WAITING状态,超时之后变为BLOCKED状态,RUNNABLE状态,执行完毕后是TERMINATED状态。这里可以倒推,最后一个执行的的线程t4,在t1执行时t4的状态是BLOCKED,同理t3执行时t1也是BLOCKED,符合图中内容。

这里可再验证一下中断的情况,让第一个等待超时并在此获取到锁的线程中断其他线程:

        List<Thread> threadList = new ArrayList<>();
        Thread thread;
        for (int i = 0; i < 5; i++) {
            thread = new Thread(() -> {
                synchronized (obj) {
                    String threadName = Thread.currentThread().getName();
                    try {
                        System.out.println(threadName + " acquire lock");
                        obj.wait(100);
                        for (int j = 0; j < 5; j++) {
                            System.out.println(threadName + " -> "
                                    + threadList.get(j).getName() + " "
                                    + threadList.get(j).getState().name());
                            if (!threadName.equals(threadList.get(j).getName())) {
                                threadList.get(j).interrupt();
                            }
                        }
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                        System.out.println(threadName +" InterruptedException");
                        for (int j = 0; j < 5; j++) {
                            System.out.println(threadName + " -> "
                                    + threadList.get(j).getName() + " "
                                    + threadList.get(j).getState().name());
                        }
                    }
                }
            }, "t-" + i);
            threadList.add(thread);
        }
        for (int i = 0; i < 5; i++) {
            threadList.get(i).start();
        }
        System.out.println("main thread end");

main thread end
t-0 acquire lock
t-3 acquire lock
t-4 acquire lock
t-2 acquire lock
t-1 acquire lock
t-3 -> t-0 TIMED_WAITING
t-3 -> t-1 BLOCKED
t-3 -> t-2 BLOCKED
t-3 -> t-3 RUNNABLE
t-3 -> t-4 BLOCKED
t-4 InterruptedException
t-4 -> t-0 BLOCKED
t-4 -> t-1 BLOCKED
t-4 -> t-2 BLOCKED
t-4 -> t-3 TERMINATED
t-4 -> t-4 RUNNABLE
t-0 InterruptedException
t-0 -> t-0 RUNNABLE
t-0 -> t-1 BLOCKED
t-0 -> t-2 BLOCKED
t-0 -> t-3 TERMINATED
t-0 -> t-4 TERMINATED
t-1 InterruptedException
t-1 -> t-0 TERMINATED
t-1 -> t-1 RUNNABLE
t-1 -> t-2 BLOCKED
t-1 -> t-3 TERMINATED
t-1 -> t-4 TERMINATED
t-2 InterruptedException
t-2 -> t-0 TERMINATED
t-2 -> t-1 TERMINATED
t-2 -> t-2 RUNNABLE
t-2 -> t-3 TERMINATED
t-2 -> t-4 TERMINATED

可以看到t2抛出异常时是RUNNABLE状态,t1执行时t2是BLOCKED状态,也符合前面说的wait方法被中断线程从waitSet中移出,然后再次尝试获取锁,获取锁之后抛出异常。

验证线程执行sleep方法后是TIMED_WAITING状态,让5个线程竞争锁,然后让竞争到锁的线程休眠,同时在主线程中一直循环查看每个线程状态:

        List<Thread> threadList = new ArrayList<>();
        Thread t;
        for (int i = 0; i < 5; i++) {
            t = new Thread(()->{
                synchronized (obj) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t-"+i);
            threadList.add(t);
        }
        boolean b = true;
        int a = 0;
        while(b || a==0) {
            for (int i = 0; i < 5; i++) {
                Thread thread = threadList.get(i);
                System.out.println(thread.getName() + " " + thread.getState().name());
                if (i == 4 && thread.getState().equals(Thread.State.TERMINATED)) b=false;
            }
            if (a == 0) {
                a++;
                for (int i = 0; i < 5; i++) {
                    threadList.get(i).start();
                }
            }
        }

控制台打印内容比较多,我截取了一部分:

t-0 NEW
t-1 NEW
t-2 NEW
t-3 NEW
t-4 NEW
t-0 RUNNABLE
t-1 RUNNABLE
t-2 RUNNABLE
t-3 RUNNABLE
t-4 RUNNABLE
t-0 TIMED_WAITING
t-1 BLOCKED
t-2 BLOCKED
t-3 RUNNABLE
t-4 RUNNABLE
t-0 TIMED_WAITING
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 BLOCKED

t-0 RUNNABLE
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 TIMED_WAITING
t-0 TERMINATED
t-1 BLOCKED
t-2 BLOCKED
t-3 BLOCKED
t-4 TIMED_WAITING

六、join

join是Thread的一个实例方法,内部实现:

    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()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

如果当前线程是活动状态,则调用当前线程的wait方法。也就是说导致线程wait返回的方式同样可以使线程join返回,例如notify、interrupt、等待超时等。join是同步方法,线程执行方法前后会自动执行加锁解锁操作,加锁对象为Thread的实例。

如果在线程t1中调用线程t2的join方法后,会发生什么?有点烧脑。

        Thread t2 = new Thread(() -> {
            System.out.println("t2 thread end");
        }, "t2");
        t2.start();
        t2.join();
        System.out.println("main thread end");

上面代码中在执行join方法前main线程(t1线程)和t2线程是并行的。执行join方法:

  • 1 执行t2对象的同步方法,首先要获取到t2对象的锁。
  • 2 获取到锁进入方法内部,循环(防止假唤醒)判断 t2线程 是否存活,t2如存活则调用wait方法,会将t1线程加入t2对象的等待集合中,同时释放锁。为什么判断的是t2是否存活,注释上说的并不是很清楚,首先isAlive是t2实例的方法,然后注释说明是“测试此线程是否存活。如果一个线程已经启动并且尚未死亡,则该线程处于活动状态。”如果这里代表t1那么t1一直满足这个存活条件,循环不会停止。
  • 3 到wait为止,t2线程还在正常执行,t1已经被加入waitSet集合了也释放了锁,停止了执行。那么什么时候t1被唤醒?t2线程执行完毕后会调用notifyAll方法(join方法的注释上说明),唤醒等待集合中线程。
  • 4 t1线程继续获取到锁,这时检测t2已经死亡,退出循环,join方法返回,t1继续执行。

直观的感觉就是t1线程在等待t2线程的执行,t2执行完毕后t1再从join方法后继续执行。

七、对比一下几个方法

方法ObjectThread
interrupt实例
interrupted静态
isInterrupted实例
wait实例
notify实例
notifyAll实例
sleep静态
yield静态
join实例

为什么wait、notify、notifyAll方法在Object中而不是Thread中?

  • 这个问题还是要从synchronized关键字说起,synchronized同步代码块或者同步方法是对某个对象加锁,JVM中每个对象的对象头中markWord中关联了一个monitor,monitor中记录了重入次数、由于获取不到锁而阻塞的线程队列、当前用于锁的线程、由于调用wait方法而进入等待的线程集合等。处于等待状态的线程是不持有锁的,需要其他线程执行notify和notifyAll方法唤醒,线程从等待集合中移出,重新准备竞争锁。
  • wait方法执行是需要释放锁的, 如果wait方法在Thread中,那么调用该方法时一定要记录下该线程是在执行同步方法(或者代码块)时进入了wait状态,因为其他线程执行notify或notifyAll唤醒这个线程后,需要重新获取这个同步方法的锁才能继续执行。另外还需要记录等待在方法上的线程,其他线程执行notify或notifyAll时需要通知这些线程去竞争锁。每个Thread对象都记录这些信息占用空间相比在Object对象占用的空间是比较大的。
  • Java中可以对任意Object对象加锁,Object作为顶级父类,wait等方法适用每个对象。
  • 另外在锁中记录那个线程正在持有锁,那些线程正在排队获取锁,那些线程在等待,要比在线程中记录获取了那个对象的锁,正在等待获取哪个对象的锁,要唤醒那些线程去竞争那些对象的锁要容易些。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值