分析 JDK 源码丨Java Thread

Thread 相关概念

线程是系统资源分配的最小单位,它被包含在进程之中,是进程中的实际运作单位。JVM 允许应用程序同时运行、执行多个线程,每个线程都有优先权,具有较高优先级的线程优先于优先级较低的线程执行

在Java中线程分为两类:User Thread(用户线程)Daemon Thread(守护线程)

在JVM启动时候会调用main函数,main函数所在的线程是就是一个用户线程,在此线程中新建的线程默认都是用户线程,但通过 Thread.setDaemon(true) 可设置守护线程(需在 Thread.start() 前调用)。守护线程是JVM中所有非守护线程的保姆,守护线程最典型的应用就是 GC (垃圾回收器)

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作,当JVM中的最后一个用户线程结束时,守护线程随着JVM一同结束


Thread 创建方式

创建线程的方式有两种:继承 Thread 类实现 Runnable 接口

Thread 类本身也是通过实现 Runnable 接口来完成创建的。创建线程时可以为线程指定名称,名称可重复。如果在创建线程时未指定名称,则会为其生成新名称

继承 Thread 类创建与启动:

     class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
         }
     }
复制代码
     PrimeThread p = new PrimeThread(143);
     p.start();
复制代码

实现 Runnable 接口创建与启动:

     class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
         }
     }
复制代码
     PrimeRun p = new PrimeRun(143);
     new Thread(p).start();
复制代码

Thread 状态变化

线程的状态可在 Thread 类源码中找到,共 6 个:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

    public enum State {

        NEW, // 尚未启动状态

        RUNNABLE, // 可运行状态,但它可能还在等待处理器资源分配

        BLOCKED, // 阻塞状态

        WAITING, // 等待状态,等待另一个线程执行完毕

        TIMED_WAITING, // 定时等待状态

        TERMINATED; // 终止状态,线程已执行完毕
    }

复制代码


Thread 类源码中的关键方法

Thread.start():启动线程

调用 start() 时,会首先检查是否是首次启动此线程,也就是threadStatus == 0,如果 threadStatus != 0 说明当前为重复启动,这是不允许的,会抛出线程状态异常错误 IllegalThreadStateException

通过校验后,会将当前线程的实例对象加入线程组 group ,之后通过本地方法 start0() 执行启动线程的操作。启动完成后在threadStartFailed()内执行移除操作 ,释放资源

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                ···
            }
        }
    }

    private native void start0();
复制代码

Thread.stop():强制停止线程执行

这个方法是不安全的,很可能会产生不可预料的结果,就好比通过断电源关机,而不是通过正常关机操作来完成,结果相同,过程却完全不同

调用 stop() 将会抛出一个ThreadDeath异常,这时候run方法也就执行结束了,线程就终止了,这种是用抛异常来结束线程的

结束后会释放子线程所持有的所有锁,一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误

    @Deprecated
    public final void stop() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }

        if (threadStatus != 0) {
            resume(); // Wake up thread if it was suspended; no-op otherwise
        }

        stop0(new ThreadDeath());
    }

    @Deprecated
    public final synchronized void stop(Throwable obj) {
        throw new UnsupportedOperationException();
    }
复制代码

Thread.run():线程实际运行的代码块

需要注意的是,如果要启动一个线程,直接调用 run() 方法是无效的,它不会产生任何实际结果

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
复制代码

Thread.interrupt():修改中断状态,以实现合理、安全的中断当前线程

stop() 方法也可中断线程,但它是立即终止,会引发一些未知的问题,所以就出现了 interrupt() 方法,用它可实现有条件的终止线程,使得数据安全得到保障

要想真正实现线程中断, interrupt() 需要配合 isInterrupted()interrupted() 一起使用,这两个方法可以获取中断标记是否为 true,获取后我们就可以做合理的处理。调用 isInterrupted() 会返回中断状态但不会还原状态, interrupted() 会返回中断状态并清除中断状态,根据实际业务需求分别使用即可

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
复制代码
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
复制代码
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
复制代码

Thread.yield():暂停执行当前线程,让出cpu执行其它线程(但是可能会被忽略)

实际上,如果想让 yield()发挥它的作用,需要搭配线程 优先级 来使用,它的实际运行流程是先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

    public static native void yield();
复制代码

Thread.wait() / Thread.wait(long):使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 或 定时等待 N 毫秒,如果没有通知就超时返回

wait() 属于 Object 对象。使用时首先需要获得锁,一般放在同步方法或同步代码块中( synchronized),由 notify()notifyAll() 唤醒

    public final void wait() throws InterruptedException {
        wait(0);
    }

    public final native void wait(long timeout) throws InterruptedException;
复制代码

Thread.join() / Thread.join(long):等待该线程结束后,再继续

其作用就是将调用join的线程优先执行,当前正在执行的线程阻塞,直到调用join方法的线程执行完毕或者被打断,主要用于线程之间的交互

    public final void join() throws InterruptedException {
        join(0);
    }

    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;
            }
        }
    }
复制代码

Thread.sleep(long):使当前线程在指定的时间内暂停执行

暂停过程中调用此线程对象的 interrupt() 会唤醒线程并抛出 InterruptedException,之后继续执行

    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);
    }
复制代码

Thread.notify() / Thread.notifyAll():唤醒正在等待状态的线程

常见使用场景是一个线程A调用了对象B的wait()方法进入等待状态,而另一个线程C调用了对象B的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。A 和 C 两个线程通过对象B来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify() 随机唤醒等待队列中等待同一共享资源的线程,此线程回退出等待队列,进入可运行状态

notifyAll() 唤醒所有正在等待队列中等待同一共享资源的全部线程,全部退出等待队列,进入可运行状态,优先级最高的开始执行

   public final native void notify();

   public final native void notifyAll();
复制代码

Thread 类源码中的其它方法

currentThread():返回当前正在执行的线程对象的引用

    /**
     * Returns a reference to the currently executing thread object.
     *
     * @return  the currently executing thread.
     */
    public static native Thread currentThread();
复制代码

这是一个 Native 方法,明这个方法是原生函数,是用C/C++语言实现的,并且被编译成了DLL,由java去调用,函数的实现体在DLL中,JDK的源代码中并不包含,所以我们看不到。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

    System.out.println("对象信息:" + Thread.currentThread());

    //// 输出结果
    对象信息:Thread[main,5,main]
复制代码

getId():返回该线程的ID

    /**
     * Returns the identifier of this Thread.  The thread ID is a positive
     * <tt>long</tt> number generated when this thread was created.
     * The thread ID is unique and remains unchanged during its lifetime.
     * When a thread is terminated, this thread ID may be reused.
     *
     * @return this thread's ID.
     * @since 1.5
     */
    public long getId() {
        return tid;
    }
    
    // tid 在 Thread 的 init 中赋值
    ···
    tid = nextThreadID();
    ···

    // tid 等于 threadSeqNumber,而 threadSeqNumber 专门被用来生成线程的 ID
    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }
复制代码

线程的ID是long类型,由nextThreadID方法生成,nextThreadID方法是线程安全的(synchronized 修饰),每次新建线程ID就++并赋值给tid

    System.out.println("线程  ID:" + Thread.currentThread().getId());

    //// 输出结果(main方法中调用)
    线程  ID:1
复制代码

**getName() :**获取线程的名称

    /**
     * Returns this thread's name.
     *
     * @return  this thread's name.
     * @see     #setName(String)
     */
    public final String getName() {
        return name;
    }
复制代码

线程名称是String类型,默认为 Thread-N (N:线程创建的顺序,从0开始)。当然了,Thread类也提供了2种修改名称的方法,即:new Thread("name") 或 Thread.setName("name")

	Thread threadTest01 = new Thread();
	System.out.println("线程名称:" + threadTest01.getName());
	
	Thread threadTest02 = new Thread();
	System.out.println("线程名称:" + threadTest02.getName());
	
	Thread threadTest03 = new Thread("我有名字,我叫 T03");
	System.out.println("线程名称:" + threadTest03.getName());
	
	Thread threadTest04 = new Thread();
	threadTest04.setName("我有名字:我叫 T04");
	System.out.println("线程名称:" + threadTest04.getName());

    //// 输出结果
    线程名称:Thread-0
    线程名称:Thread-1
    线程名称:我有名字,我叫 T03
    线程名称:我有名字:我叫 T04

复制代码

getPriority():获取线程优先级

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

    // 线程优先级在初始化(init)时设置,默认是等同于父线程优先级
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        Thread parent = currentThread();
        ···
        this.priority = parent.getPriority();
        ···
    }

    /**
     * Returns this thread's priority.
     *
     * @return  this thread's priority.
     * @see     #setPriority
     */
    public final int getPriority() {
        return priority;
    }
复制代码

线程优先级默认为父线程的优先级 :this.priority = parent.getPriority(); 。线程的优先级不能决定线程的执行次序,但较高的优先级获取CPU资源的概率较大

线程的优先级可通过 setPriority(int newPriority) 设置,参数的取值范围为 1 - 10,默认为 5

    public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }
复制代码

需要注意的是,设置线程优先级时,不能大于最大优先级,否则会发生throw new IllegalArgumentException();,也不能大于所在线程组的最高优先级,否则将被重置为线程组的优先级newPriority = g.getMaxPriority();,如下:

public static void main(String[] args) {
	
	ThreadMethods main = new ThreadMethods();
	Thread t01 = main.new MyThread01();
	Thread t02 = main.new MyThread02();
	
	t01.setPriority(Thread.MAX_PRIORITY);
	t02.setPriority(Thread.MIN_PRIORITY);
	
	t02.start();
	t01.start();
}

/**
 * @des 测试线程 01
 * */
class MyThread01 extends Thread{
	public void run() {
		super.run();
		
		for(int i = 0 ; i < 10 ; i++ ){
			System.out.println("MyThread01:" + i);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
} 

/**
 * @des 测试线程 02
 * */
class MyThread02 extends Thread{
	public void run() {
		super.run();
		
		for(int i = 0 ; i < 10 ; i++ ){
			System.out.println("MyThread02:" + i);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
} 

//// 输出结果
MyThread01:0
MyThread02:0
MyThread02:1
MyThread01:1
MyThread02:2
MyThread01:2
MyThread02:3
MyThread01:3
MyThread01:4
MyThread02:4
MyThread01:5
MyThread02:5
MyThread01:6
MyThread02:6
MyThread01:7
MyThread02:7
MyThread01:8
MyThread02:8
MyThread01:9
MyThread02:9
复制代码

Thread 使用中常见问题

死锁产生的原因以及如何避免和解决

死锁产生的原因是多个线程同时被阻塞的情况下,它们中的一个或全部都在等待某个资源被释放,由于线程被无限阻塞,其余线程不可能等到此资源被释放,因此程序不能再继续正常运行(举个生动的栗子:一个装载有价值连城宝藏的宝箱需要两把钥匙才能打开,有两个人各有一把钥匙,但是互相都在等待对方先交出钥匙,但他们谁都不交出自己的钥匙并就此一直僵持下去)

为什么产生死锁?

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生

  • 互斥条件:线程要求对所分配的资源进行排他性控制,即在一段时间内某 资源仅为一个进程所占有.此时若有其他进程请求该资源.则请求进程只能等待.
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放).
  • 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放.
  • 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

死锁产生条件总结:

  1. 有至少一个资源不能共享
  2. 至少有一个任务必须持有一个资源并且等待获取另一个被别的任务持有的资源
  3. 资源不能任务抢占
  4. 必须有循环等待

如何避免和解决死锁?

1、避免嵌套锁 这是死锁最常见的原因,如果您已经持有一个资源,请避免锁定另一个资源。如果只使用一个对象锁,则几乎不可能出现死锁情况,比如以下代码对上边的循环嵌套部分进行修改,则避免了死锁的情况:

public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on " + obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on " + obj1);
            work();
        }
        System.out.println(name + " released lock on " + obj1);
        System.out.println(name + " acquiring lock on " + obj2);
        synchronized (obj2) {
            System.out.println(name + " acquired lock on " + obj2);
            work();
        }
        System.out.println(name + " released lock on " + obj2);

        System.out.println(name + " finished execution.");
    }
复制代码

2、只锁需要的部分 只获对需要的资源加锁,例如在上面的程序中,我们锁定了完整的对象资源,但是如果我们只需要其中一个字段,那么我们应该只锁定那个特定的字段而不是完整的对象

3、避免无限期等待 如果两个线程使用 thread join 无限期互相等待也会造成死锁,我们可以设定等待的最大时间来避免这种情况。

JAVA 中锁的种类与区别

在代码执行过程中,一些数据需要进行排他性的控制以保证最终计算结果的正确性,所以需要有一种机制保证在执行过程中此数据被锁住不会被外界修改,这种机制就是锁机制

同时,根据锁的特性、设计、状态不同,又可以不严格的分为以下几类:

公平锁/非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,可能会造成优先级反转或饥饿显现

Synchronized 是非公平锁,ReentrantLock 通过构造函数可以决定是公平锁还是非公平锁,模式是非公平锁

非公平锁的吞吐量性能比公平锁要更大

可重入锁

也叫递归锁,指同一线程在外层方法获取锁的时候,进入内层方法会自动获取锁

Synchronized 和 ReentranLock 都是可重入锁,可在一定程度上避免死锁

独享锁/共享锁

独享锁是指该锁一次只能被一个县城持有,共享锁是指该锁可以被多个想成持有

Synchronized 和 ReentranLock 都是独享锁。ReadWriteLock 的读锁是共享锁,写锁是独占锁。ReentrantLock 的独享锁和共享锁也是通过 AQS 来实现的

互斥锁/读写锁

互斥锁 = 独享锁,读写锁 = 共享锁。互斥锁实质就是 ReentrantLock,读写锁实质就是 ReadWriteLock

乐观锁/悲观锁

它们不属于具体的锁分类,而是看待并发同步的角度

乐观锁认为对于同一数据的并发操作是不会发生修改的,在更新数据的时候回采用不断的尝试更新,乐观锁认为不加锁的并发操作是没事的

悲观锁认为对于同一个数据的并发操作一定是会发生修改的,因此对于统一数据的并发操作,悲观锁采取加锁形式,因为悲观锁认为不加锁的操作一定会有问题

悲观锁适合操作非常多的场景,乐观锁适合读写非常多的场景,不加锁可以大大提高性能

分段锁

其实是一种锁的策略,不是具体锁。如 ConcurrentHashMap 并发的实现就是通过分段锁的形式来实现高效并发操作

当要 put 元素时并不是对整个 hashMap 加锁,而是先通过 hashCode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在不同分段就是做到真正的并行插入,但是统计 size 时就需要获取所有的分段锁才能计算

分段锁的设计是为了细化锁的粒度

偏向锁/轻量级锁/重量级锁

这是按照锁状态来归纳的,并且是针对 Synchronized 的。java 1.6 为了减少获取锁是释放锁带来的性能问题引入了一种状态,它会随着竞争情况逐渐升级,锁可以升级但不可降级,意味着偏向锁升级成轻量级锁后无法回撤,这种升级无法降级的策略目的就是为了提高活的锁和释放锁的效率

自旋锁

其实是相对于互斥锁的概念,互斥锁线程会进入 WAITING 状态和 RUNNABLE 状态的奇幻,涉及上下文切换、CPU抢占等开销,自旋锁的线程一直是 RUNNABLE 状态,一直在那循环检测锁标志位,机制不重复,但是自旋锁加锁全程消耗 CPU,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长

可中断锁

Synchronized 是不可中断的,Lock 是可中断的

这里的可中断建立在阻塞等待中断,运行中是无法中断的


Synchronized 同步锁原理

本篇将持续更新线程相关知识,一起查漏补缺学个痛快!

猜你喜欢

转载自juejin.im/post/5d4d41945188252d336992d4