java进阶-多线程

后端 / 笔记 / 2021-10-01

join 底层原理

package thread;

public class ThreadJoin {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "running.."), "t1");
        Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "running.."), "t2");
        Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + "running.."), "t3");
        t1.start();
        t2.start();
        t3.start();
    }
}

思考上面代码执行结果是多少?

有序,还是乱序?

t1running..
t3running..
t2running..

因为执行顺序是靠cpu调度的,所以结果不确定性。

那么如何顺序执行呢

join() 方法的作用,就是等待这个线程的结束,谁调用它,阻塞谁。

package thread;

public class ThreadJoin {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " running..");
        }, "t1");
        // t2 等t1
        Thread t2 = new Thread(() -> {
            // 等待t1执行完毕
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " running..");
        }, "t2");
        // t3 等t2
        Thread t3 = new Thread(() -> {
            // 等待t2执行完毕
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " running..");
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

执行结果

t1 running..
t2 running..
t3 running..

那么这个join如何做到的?

我查看其java实现

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

通过简单分析后我们不难发现,join底层是wait实现的。
那么上面就解释通了:

join 底层原理是基于wait封装的,唤醒代码在 jvm中。
当jvm在关闭线程前会检测阻塞线程,然后执行notifyAll

那么如何知道notify不是java实现的呢?

阅读jvm源码

void Threads::remove(JavaThread* p, bool is_daemon) {
  // Extra scope needed for Thread_lock, so we can check
  // that we do not remove thread without safepoint code notice
  { MonitorLocker ml(Threads_lock);

    // BarrierSet state must be destroyed after the last thread transition
    // before the thread terminates. Thread transitions result in calls to
    // StackWatermarkSet::on_safepoint(), which performs GC processing,
    // requiring the GC state to be alive.
    BarrierSet::barrier_set()->on_thread_detach(p);

    assert(ThreadsSMRSupport::get_java_thread_list()->includes(p), "p must be present");

    // Maintain fast thread list
    ThreadsSMRSupport::remove_thread(p);

    _number_of_threads--;
    if (!is_daemon) {
      _number_of_non_daemon_threads--;

      // Only one thread left, do a notify on the Threads_lock so a thread waiting
      // on destroy_vm will wake up.
      if (number_of_non_daemon_threads() == 1) {
        ml.notify_all();
      }
    }
    ThreadService::remove_thread(p, is_daemon);

    // Make sure that safepoint code disregard this thread. This is needed since
    // the thread might mess around with locks after this point. This can cause it
    // to do callbacks into the safepoint code. However, the safepoint code is not aware
    // of this thread since it is removed from the queue.
    p->set_terminated(JavaThread::_thread_terminated);

    // Notify threads waiting in EscapeBarriers
    EscapeBarrier::thread_removed(p);
  } // unlock Threads_lock

  // Reduce the ObjectMonitor ceiling for the exiting thread.
  ObjectSynchronizer::dec_in_use_list_ceiling();

  // Since Events::log uses a lock, we grab it outside the Threads_lock
  Events::log(p, "Thread exited: " INTPTR_FORMAT, p2i(p));
}


// NOTE: see comment of notify()
void ObjectSynchronizer::notifyall(Handle obj, TRAPS) {
  JavaThread* current = THREAD;

  markWord mark = obj->mark();
  if (mark.has_locker() && current->is_lock_owned((address)mark.locker())) {
    // Not inflated so there can't be any waiters to notify.
    return;
  }
  // The ObjectMonitor* can't be async deflated until ownership is
  // dropped by the calling thread.
  ObjectMonitor* monitor = inflate(current, obj(), inflate_cause_notify);
  monitor->notifyAll(CHECK);
}


验证猜想

package thread;

public class ThreadInterrupt {
    private static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            synchronized (object) {
                System.out.println("1");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("2");
            }
        });
        thread.start();
        Thread.sleep(3_000);
        thread.interrupt();
    }
}


1
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at thread.ThreadInterrupt.lambda$main$0(ThreadInterrupt.java:10)
	at java.lang.Thread.run(Thread.java:748)
2

Process finished with exit code 0

当执行interrupt时线程被打断,执行了jvm的Threads::remove方法,当线程关闭时,jvm会主动执行notifyAll来保证事情在可控范围之内。

线程的七种状态

话不多说看jvm

// Java Thread Status for JVMTI and M&M use.
// This thread status info is saved in threadStatus field of
// java.lang.Thread java class.
enum class JavaThreadStatus : int {
  NEW                      = 0,
  RUNNABLE                 = JVMTI_THREAD_STATE_ALIVE +          // runnable / running
                             JVMTI_THREAD_STATE_RUNNABLE,
  SLEEPING                 = JVMTI_THREAD_STATE_ALIVE +          // Thread.sleep()
                             JVMTI_THREAD_STATE_WAITING +
                             JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                             JVMTI_THREAD_STATE_SLEEPING,
  IN_OBJECT_WAIT           = JVMTI_THREAD_STATE_ALIVE +          // Object.wait()
                             JVMTI_THREAD_STATE_WAITING +
                             JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
                             JVMTI_THREAD_STATE_IN_OBJECT_WAIT,
  IN_OBJECT_WAIT_TIMED     = JVMTI_THREAD_STATE_ALIVE +          // Object.wait(long)
                             JVMTI_THREAD_STATE_WAITING +
                             JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                             JVMTI_THREAD_STATE_IN_OBJECT_WAIT,
  PARKED                   = JVMTI_THREAD_STATE_ALIVE +          // LockSupport.park()
                             JVMTI_THREAD_STATE_WAITING +
                             JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
                             JVMTI_THREAD_STATE_PARKED,
  PARKED_TIMED             = JVMTI_THREAD_STATE_ALIVE +          // LockSupport.park(long)
                             JVMTI_THREAD_STATE_WAITING +
                             JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                             JVMTI_THREAD_STATE_PARKED,
  BLOCKED_ON_MONITOR_ENTER = JVMTI_THREAD_STATE_ALIVE +          // (re-)entering a synchronization block
                             JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER,
  TERMINATED               = JVMTI_THREAD_STATE_TERMINATED
};

#endif // SHARE_CLASSFILE_JAVATHREADSTATUS_HPP

从上面源码可以看线程一共有七种状态

image.png

  • 新生态: 创建线程
  • 就绪态: 执行start方法
  • 运行态: 线程开始执行
  • 阻塞态: 没有获得锁
  • 等待: 等待线程
  • 等待超时: 等待线程超时
  • 消亡:线程正常/异常 结束
  • 没有获得锁的线程会等待
  • 持有锁的线程释放锁时,会唤醒正在阻塞的线程
  • wait会主动释放锁,线程进入阻塞状态。
  • 使用LockSupport也可以让线程进入唤醒/阻塞状态。
  • yeild 主动放弃cpu调度执行权,线程进入就绪态。

如何防止sleep导致cpu飙升?

因为线程运行在cpu之上

package thread;

public class ThreadSleep {
    public static void main(String[] args) {
        // 当前电脑 6核 开启 8个线程
        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                while (true) {

                }
            }).start();
        }

    }
}

然后我们通过jconsole查看cpu占用

image.png

可以看到cpu占用还是挺高的。

如何解决?

添加一个sleep语句试试

package thread;

public class ThreadSleep {
    public static void main(String[] args) {
        // 当前电脑 6核 开启 8个线程
        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }
}

继续通过jconsole查看cpu占用

image.png

差别还是蛮大的

为什么会出现如此大的差距呢?

当我们加sleep方法会减缓cpu的调度压力,所以在日常生活中尽量少写死循环代码,如要非要写,那么记得添加sleep方法。

用户线程和守护线程

在java中有两种线程:分别是用户线程守护线程

守护线程依赖于,用户线程,用户线程退出了守护线程也会退出(燕子 燕子 没你我怎么活啊),最典型的守护线程:垃圾回收线程.

用户线程是独立存在的,不会因为其他用户线程退出而退出。

package thread;

public class ThreadDaemon {
    public static void main(String[] args) {
        new Thread(()->{while (true){}},"sub thread").start();
        System.out.println("main thread run over");
    }
}

main thread run over

问题:为什么主线程结束了子线程还在运行?
答:因为子线程是用户线程,不会被其他线程退出影响

如果我要修改代码逻辑为:主线程运行完毕子线程随之结束,如何做?

子线程修改为守护线程

subThread.setDaemon(true);

package thread;

public class ThreadDaemon {
    public static void main(String[] args) {
        Thread subThread = new Thread(() -> {
            while (true) {
            }
        }, "sub thread");
        subThread.setDaemon(true);
        subThread.start();

        System.out.println("main thread run over");
    }
}

Thread.currentThread().setDaemon(true); 也可以
这样在主线程结束时子线程也结束了。

如何安全停止一个线程

stop? stop会清除线程监控器锁的信息,会导致线程安全问题。不建议采用

interrupt? interrupt会打断正在运行/阻塞的线程。如果目标线程在阻塞状态,那么 interrupt生效该线程的中断状态呗清除,抛出InterruptException异常。 不建议采用


正确方法:设置一个线程标志位然后通过标志位来进行判断。

package thread;

public class ThreadStop extends Thread{
    private boolean alive = true;

    public void exit(){
        this.alive=false;
    }

    @Override
    public void run() {
        while (alive){
           if (this.isInterrupted()){
               break;
           }
        }
        System.out.println("over");
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadStop thread = new ThreadStop();
        thread.start();
        
        Thread.sleep(3_000);
        thread.exit();
    }
}

over

Lock锁

比 synchronized 更灵活


区别

  • synchronized锁基于jvm实现 推荐
  • lock 基于AQS实现 重量级

用法

lock锁的获取释放是需要开发人员自己决定

用之前先看看有啥

Lock.java


package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;

public interface Lock {

    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock

获取锁

package thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            count();
        }, "thread1").start();
        Thread.sleep(1_000);
        new Thread(() -> {
            count();
        }, "thread2").start();
    }

    public static void count() {
        System.out.println(Thread.currentThread().getName() + " try lock");
        // 上锁
        lock.lock();
        System.out.println(Thread.currentThread().getName() + " locked success");
    }
}

thread1 try lock
thread1 locked success
thread2 try lock

我们可以看到 thread1 获取锁后,thread2一直在尝试获取锁。
话说 thread1 不能总是占用公共资源吧,那如何释放呢? unlock

unlock

释放锁

package thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            count();
            try {
                Thread.sleep(2_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // thread1 释放锁
                lock.unlock();
            }
        }, "thread1").start();
        Thread.sleep(1_000);
        new Thread(() -> {
            count();
        }, "thread2").start();
    }

    public static void count() {
        System.out.println(Thread.currentThread().getName() + " try lock");
        // 上锁
        lock.lock();
        System.out.println(Thread.currentThread().getName() + " locked success");
    }
}

thread1 try lock
thread1 locked success
thread2 try lock
thread2 locked success

当thread1释放掉锁后 thread2 立马拿到了锁

设计思想: 俗话说得好:好借好还,再借不难 既然你要获取锁,那么一般情况下你要考虑释放锁,年轻人要讲5的

这里 tryLock 两个方法就不讲了 根据字面意思也可以理解,尝试获取锁,另一个方法就是有个等待超时,一旦超时则抛出异常。
我们知道 用 synchronized 锁 线程之间的 通信 用 waitnofity 来解决,那么lock锁如何在线程之间通信呢? condition

condition

  • await 进入阻塞状态等待唤醒
  • signal 唤醒进入阻塞状态的线程

需要注意的是这两个方法使用的前提是在拿到锁后使用哦

package thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLockCondition {
    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static Thread build(){
        return new Thread(() -> {
            try {
                // 释放锁区前先上锁
                System.out.println(Thread.currentThread().getName()+" try lock");
                lock.lock();
                // 释放锁 && 当前线程进入阻塞状态
                condition.await(); // 我睡一会 一会叫醒我
                System.out.println(Thread.currentThread().getName()+" waiting");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName()+" unlocked");
            }
        });
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = build();
        thread1.start();

        Thread.sleep(2_000);
        // 唤醒 阻塞线程
        lock.lock();
        condition.signal(); // 醒醒快醒醒
        lock.unlock();

    }
}

线程礼让 yield

多线程 yield 会让线程从运行状态进入就绪状态,礼让后调度执行其他线程。

package thread;

public class ThreadYield extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+" release cpu schedule");
            // 开始礼让
            this.yield();
        }
    }

    public static void main(String[] args) {
        new ThreadYield().start();
        new ThreadYield().start();
    }
}

Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule

可以看到线程1和线程2互相让,儒学思想多好

线程优先级

线程优先级决定了cpu调度的优先级,优先级越高,cpu调度越频繁。

thread1.setPriority(Thread.MAX_PRIORITY);

package thread;

public class ThreadYield extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " release cpu schedule");
            // 开始礼让
            this.yield();
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadYield thread1 = new ThreadYield();
        ThreadYield thread2 = new ThreadYield();

        thread1.setPriority(Thread.MAX_PRIORITY);
        thread1.start();
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread2.start();

    }
}
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule
Thread-1 release cpu schedule
Thread-0 release cpu schedule

可以看到线程1明显调度次数比次线程2频繁,因为线程1优先级比线程2高

wait和sleep区别

共同点:

都可以将线程从运行状态变为就绪状态

不同点:

  • wait 是会释放当前对象锁的,前提是要拿到对象锁
  • sleep 不会释放当前对象锁