第一部分 多线程基础

  • 本系列博客,主要是面向Java8的源码。
  • 本系列博客主要参考汪文君老师 《Java高并发编程详解》一书
  • 转载请注明出处,多谢~。

1. 线程的start方法剖析

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 */
public synchronized void start() {
        
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        
        group.add(this);

        boolean started = false;
        try {
            start0();//核心部分就是这个start0本地方法,也就是JNI方法,run方法就是被此方法调用
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                
            }
        }
    }

2. Runnable接口的引入

//Runnable接口非常简单,只定义了一个无参数无返回值的run方法
public interface Runnable {
    public abstract void run();
}

可以通过继承Thread然后重写run方法实现自己的业务逻辑,也可以实现Runnable接口实现

//构造Thread时传递Runnable
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        
    }
});

//Lambda简化
Thread thread = new Thread(() -> {

});

注意:

  • 准确地讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式
  • Thread负责线程本身相关的职责和控制,而Runnable则负责逻辑执行单元的部分

3. 线程的父子关系

Thread的所有构造函数,最终都会调用一个方法 init()

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread(); //获取当前线程作为父线程
    SecurityManager security = System.getSecurityManager();
    ......
}

结论:

  • 一个线程的创建肯定是由另一个线程完成的
  • 被创建的线程的父线程就是创建它的线程

4. Thread与ThreadGroup

在Thread的构造函数中,可以显示的指定线程的Group,也就是ThreadGroup

//接着读Thread init方法的源码,你就会发现
SecurityManager security = System.getSecurityManager();
if (g == null) {
    /* Determine if it's an applet or not */

    /* If there is a security manager, ask the security manager
               what to do. */
    if (security != null) {
        g = security.getThreadGroup();
    }

    /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
    if (g == null) {
        g = parent.getThreadGroup();
    }
}

结论:

  • main线程所在的ThreadGroup成为main
  • 构造一个线程时如果没有显示的指定ThreadGroup,那么它将会和父线程同属于一个ThreadGroup

5. 守护线程

守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程

JVM程序在什么情况下会退出?

The Java Virtual Machine exits when the only threads running are all daemon threads.

在正常退出的情况下,若JVM中没有一个非守护线程,则JVM的进程会退出

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.setDaemon(true); //将thread设置为守护线程
        thread.start(); //启动thread线程
        Thread.sleep(2000L);
        System.out.println("Main thread exit");
    }
}

运行结果:main线程结束生命周期后,JVM也随之退出运行

6. Thread API

6.1 线程sleep
//sleep是一个静态方法,会让当前线程进入指定毫秒数的休眠
//注意:休眠不会放弃monitor锁的所有权
Thread.sleep()
    
//使用TimeUnit代替Thread.sleep
TimeUnit.HOURS.sleep(3)//休眠3小时
TimeUnit.MINUTES.sleep(24)//休眠24分钟
TimeUnit.SECONDS.sleep(17)//休眠17秒
TimeUnit.MILLISECONDS.sleep(12)//休眠12毫秒
6.2 线程yield
//yield方法属于一种启发式的方法
//其会提醒调度器我愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略 RUNNING->RUNNABLE
public static native void yield();

注意:

  • sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗
  • yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
  • sleep会使线程短暂block,会在给定的时间内释放CPU资源
  • 一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会
6.3 设置线程优先级

public final void setPriority(int newPriority) 为线程设置优先级

public final int getPriority() 获取线程的优先级

源码分析:

public final static int MAX_PRIORITY = 10;
public final static int MIN_PRIORITY = 1;
public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { //线程的优先级不能小于1也不能大于10
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        //如果线程的优先级大于线程所在group的优先级,指定的优先级就会失效,取而代之的是group的最大优先级
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}
6.4 获取线程ID

public long getId()

线程的ID在整个JVM进程中都会是唯一的,并且都是从0开始逐次递增

自己创建的线程绝非第0号线程

6.5 获取当前线程

public static native Thread currentThread();

用于返回当前执行线程的引用

6.6 线程interrupt

public void interrupt()

public static boolean interrupted() 判定当前线程是否被中断

public boolean isInterrupted() 判定当前线程是否被中断

以下方法的调用会使得当前线程进入阻塞状态,调用interrupt方法可以打断阻塞

  • Object的wait()方法
  • Object的wait(long) 方法
  • Object的wait(long,int)方法
  • Thread的sleep(long)方法
  • Thread的sleep(long,int)方法
  • Thread的join()方法
  • Thread的join(long)方法
  • Thread的join(long,int)方法
  • InterruptibleChannel的io操作
  • Selector的wakeup方法
  • 其他方法

上述的方法都会使得当前线程进入阻塞状态

另外一个线程调用被阻塞线程的interrupt方法可以打断阻塞

因此这些方法有时候被称为可中断的方法

注意:仅仅打断了线程的阻塞状态,不等于生命周期结束

interrupt到底做了什么?

①:在一个线程内部存在着名为interrupt flag的标识

②:如果一个线程被interrupt,那么它的flag将被设置

③:线程执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除

④:如果一个线程是死亡状态,iterrupt会直接被忽略

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                //空的死循环,不会捕捉中断信号
            }
        });

        thread.start(); //启动thread线程
        Thread.sleep(2000L);
        System.out.println(thread.isInterrupted());//false
        thread.interrupt();//
        Thread.sleep(2000L);
        System.out.println(thread.isInterrupted());//true
    }
}


public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000L);//可中断方法,会捕捉到中断信号,并且擦去interrupt标识
                } catch (InterruptedException e) {
                    System.out.println("I am be interrupted");
                }
            }
        });

        thread.start(); 
        Thread.sleep(2000L);
        System.out.println(thread.isInterrupted());//false
        thread.interrupt();
        Thread.sleep(2000L);
        System.out.println(thread.isInterrupted());//false
    }
}

interrupted()isInterrupted()区别

interrupted是一个静态方法,虽然其也用于判断当前线程是否被中断,但是调用该方法会直接擦除线程的interrupt标识

如果当前线程被打断,第一次调用Interrupt方法会返回true,并且擦除interrupt标识,第二次之后的调用都会返回false,除非再次被打断

public class ThreadInterrupted {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println(Thread.interrupted());
            }
        });
        thread.setDaemon(true);
        thread.start();
        TimeUnit.MILLISECONDS.sleep(2);
        thread.interrupt();
    }
}

/**
* 输出
* ...
* false
* false
* true
* false
* false
* ...
*/

interrupt注意事项:

//interrupted源码
public static boolean interrupted() {
    return currentThread().isInterrupted(true); //擦除interrupt标识
}
//isInterrupted源码
public boolean isInterrupted() {
    return isInterrupted(false); //不擦除interrupt标识
}
//都调用了isInterrupted这个本地方法
private native boolean isInterrupted(boolean ClearInterrupted);//ClearInterrupted主要来控制是否擦除interrupt标识

分析下面两段程序,有什么不同?

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        // 1,判断当前线程是否被中断
        System.out.println("Main Thread is interrupted? " + Thread.interrupted());

        //2,中断当前线程
        Thread.currentThread().interrupt();

        //3,判断当前线程是否已经被中断
        System.out.println("Main Thread is interrupted? " + Thread.currentThread().isInterrupted());

       try {
           //4,执行可中断方法
           TimeUnit.SECONDS.sleep(10);
       } catch (InterruptedException e) {
           System.out.println("Interrupted Exception");
       }
    }
}
//输出
// Main Thread is interrupted? false
// Main Thread is interrupted? true
// Interrupted Exception

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 1,判断当前线程是否被中断
        System.out.println("Main Thread is interrupted? " + Thread.currentThread().isInterrupted());

        //2,中断当前线程
        Thread.currentThread().interrupt();

        //3,判断当前线程是否已经被中断
        System.out.println("Main Thread is interrupted? " + Thread.interrupted());

       try {
           //4,执行可中断方法
           TimeUnit.SECONDS.sleep(10);
       } catch (InterruptedException e) {
           System.out.println("Interrupted Exception");
       }
    }
}
//输出
//Main Thread is interrupted? false
//Main Thread is interrupted? true

可以看出Demo2的可中断方法没有抛出异常,意味着没有被中断,而Demo1被中断了

原因:

  • interrupted()立即擦除了Interrupt标识
  • isInterrupted()捕获到了InterruptedException之后才会擦除Interrupt标识
6.7 线程join

可中断方法

join某个线程A,会使当前线程B进入等待,知道A生命周期结束,或者到达给定时间,B此时是BLOCKED的

public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {
        //1.定义两个线程
        List<Thread> threads = IntStream.range(1, 3).mapToObj(ThreadJoin::create).collect(Collectors.toList());

        //2.启动两个线程
        threads.forEach(Thread::start);

        //3.执行这两个线程的join方法
        for (Thread thread : threads) {
            thread.join();
        }

        //4.main线程循环输出
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "#" + i);
            shortSleep();
        }
    }

    //构造一个简单线程,每个线程只是简单的循环输出
    private static Thread create(int seq) {
        return new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "#" + i);
                shortSleep();
            }
        }, String.valueOf(seq));
    }

    private static void shortSleep() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行上面的代码:

//输出             线程一和线程二交替输出直到生命周期结束,main线程才开始运行
...
2#8
1#8
1#9
2#9
main#0
main#1
main#2
...
//注释掉3			线程一和线程二和main线程交替执行

总结:join方法会使当前线程永远的等待下去,直到期间被另外的线程中断,或者join的线程执行结束,或者join的另外两个重载方法指定的毫秒数时间到达后,当前线程才会退出阻塞

6.8 如何关闭一个线程
6.8.1 正常关闭
  1. 线程结束生命周期正常结束

  2. 捕获中断信号关闭线程

    public class Way1 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread() {
                @Override
                public void run() {
                    while (!isInterrupted()) {
                        //working
                    }
                }
            };
            t.start();
            TimeUnit.MINUTES.sleep(1);
            t.interrupt();
        }
    }
    
    public class Way2 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                for (; ; ) {
                    //working
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            });
            t.start();
            TimeUnit.MINUTES.sleep(1);
            t.interrupt();
        }
    }
    
  3. 使用volatile开关控制

    public class FlagThreadExit {
        static class MyTask extends Thread {
            private volatile boolean closed = false;
    
            @Override
            public void run() {
                System.out.println("I will start work");
    
                while (!closed && !isInterrupted()) {
                    //working...
                }
    
                System.out.println("I will stop work");
            }
    
            public void close() {
                closed = true;
                this.interrupt();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            MyTask t = new MyTask();
            t.start();
            TimeUnit.MINUTES.sleep(1);
            System.out.println("System will be end");
            t.close();
        }
    }
    

    上面的例子定义了一个closed开关变量,并且是用volatile修饰,同样可以关闭线程

6.8.2 异常退出

在一个线程的执行单元中,是不允许抛出checked异常的,不论Thread的run方法还是Runnable的run方法,如果线程运行过程中需要捕获checked异常并且判断是否还有运行下去的必要,那么可以将checked异常封装成unchecked异常(RuntimeException)抛出进而结束线程的生命周期

6.8.3 进程假死

所谓假死,就是进程虽然存在,但是没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上并没有死
一般是由于某个线程阻塞了或者出现死锁的情况

7. 线程安全与数据同步

7.1 什么是 synchronized ?

sychronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读和写都将通过同步的方式来进行。

  • synchronized 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现
  • synchronized 关键字包括monitor entermonitor exit两个 JVM 指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
  • synchronized 的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter

JVM指令分析

package com.xp;

import java.util.concurrent.TimeUnit;

public class Mutex {
    private final static Object MUTEX = new Object();

    public void accessResource() {
        synchronized (MUTEX) {
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        for (int i = 0; i < 5; i++) {
            new Thread(mutex::accessResource).start();
        }
    }
}

使用JDK命令javap 对Mutex class进行反汇编,输出了大量的JVM指令,在这些指令中,你将发现monitor enter 和monitor exit是成对出现的(有些时候会出现一个monitor enter 多个monitor exit,但是每一个monitor exit 之前必有对应的monitor enter,这是肯定的)

(1) Monitorenter

每个对象都与一个monitor 相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情。

  • 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了。
  • 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
  • 如果monitor 已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。

(2) Monitorexit

释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该 monitor block的线程将再次尝试获得对该monitor的所有权。

注意:

  • 与monitor关联的对象不能为空

  • synchronized作用域要尽可能小,尽可能只作用于共享资源的读写作用域

  • 不同的monitor企图锁相同的方法是不现实的

  • 多个锁交叉容易导致死锁产生

7.2 程序死锁的原因
  • 交叉锁可导致程序出现死锁
  • 内存不足
  • 一问一答式数据交换
  • 数据库锁
  • 文件锁
  • 死循环引起的死锁

8. 线程间通信

8.1 wait和notify方法详解

wait和notify并不是Thread特有的方法,而是Object中的方法,也就是说在JDK中的每一个类都拥有着两个方法

wait三个重载方法:

public final void wait() throws InterruptedException;

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException;

  • wait方法的这三个重载方法都将调用wait(long timeout)这个方法,前文使用的wait()方法等价于 wait(0),0代表着永不超时
  • Object 的wait(long timeout)方法会导致当前线程进入阻塞,直到有其他线程调用了 Object的notify()或者notifyAll()方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。
  • wait 方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中使用。
  • 当前线程执行了该对象的wait方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,它就会释放对该对象monitor的所有权,其他线程也会有机会继续争抢该monitor的所有权。

notify方法:

public final native void notify();

  • 唤醒单个正在执行该对象wait方法的线程。
  • 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。
  • 被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行。

注意:

  • wait方法是可中断方法,这也就意味着,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的;可中断方法被打断后会收到中断异常 InterruptedException,同时interrupt标识也会被擦除。
  • 线程执行了某个对象的wait方法以后,会加人与之对应的wait set中,每一个对象的monitor 都有一个与之关联的wait set。
  • 当线程进入wait set 之后,notify方法可以将其唤醒,也就是从 wait set中弹出,同时中断wait中的线程也会将其唤醒。
  • 必须在同步方法中使用wait和notify方法,因为执行wait 和notify的前提条件是必须持有同步方法的monitor的所有权,也就是必须在同步方法中使用
  • 同步代码的monitor必须与执行wait notify方法的对象一致
8.2 wait和sleep的异同

从表面上看,wait 和sleep方法都可以使当前线程进入阻塞状态,但是两者之间存在着本质的区别,下面我们将总结两者的区别和相似之处。

  • wait 和sleep方法都可以使线程进入阻塞状态。
  • wait 和 sleep方法均是可中断方法,被中断后都会收到中断异常。
  • wait 是Object的方法,而sleep是Thread特有的方法。
  • wait方法的执行必须在同步方法中进行,而sleep则不需要。
  • 线程在同步方法中执行 sleep方法时,并不会释放monitor的锁,而 wait方法则会释放monitor的锁。
  • sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值