了解线程

     并发处理的广泛应用是使得 Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力最有力的武器。线程也是一把双刃剑,帮我们充分利用cpu高效快速的完成任务的同时也带来了线程安全和性能相关的问题。

1、线程的创建

    在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞,通过对不同任务创建不同的线程去处理,充分利用cpu,可以提升程序处理的实时性,线程可以认为是轻量级的进程,所以线程的创建、销毁 比进程更快。线程的创建一般有以下四种方法:
  • 1.继承Thread类,重写run方法(其实Thread类本身也实现了Runnable接口);
  • 2.实现Runnable接口,重写run方法;
  • 3.实现Callable接口,重写call方法(有返回值);
  • 4.使用线程池(有返回值);

1.1、继承Thread类

线程是进程的一个执行单元,本质都是在实现一个线程任务。通过源码可知,Thread类其实也是实现了Runnable接口,代表一个线程的实例:
具体实现通过JDK提供的Thread类,继承Thread类,重写Thread类的run方法即可。步骤:
   (1) 继承thread类,实现run() 方法,具体要完成的task;  
   (2) 启动线程,new Thread子类().start();
这里创建一个新的线程,都要新建一个Thread子类的对象,创建线程实际调用的是父类Thread空参的构造器,具体实现如下:
//todo 继承Thread类创建线程
public class ExtentThreadTest extends Thread {
   
    @Override
    public void run() {
        //TODO  实现任务task
        System.out.println("线程run:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
         new ExtentThreadTest().start(); //运行线程
    }

}

1.2、实现Runnable接口

由于java单继承的特性,如果类已经 extends 一个实现类,就无法再继承 第二个类Thread,此时,可以通过实现一个Runnable接口。其实Runnable就是一个线程任务,使线程任务的执行和线程的控制解耦,这是推荐也比较常用的一种创建线程任务的方式。
Runnable函数式接口:
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

具体实现步骤如下:

  • (1) 定义一个线程任务类来实现Runnable接口;
  • (2) 实现run()方法,方法体中的代码就是所执行的task;
  • (3) 创建线程控制类thread类,将任务传入Thread类;
  • (4) 启动线程;
//实现Runnable接口
public class RunnableThreadTest implements Runnable {

    private static Logger log = LoggerFactory.getLogger(RunnableThreadTest.class);

    //实现run方法
    @Override
    public void run() {
        log.info("Runnable thread test");
    }

    public static void main(String[] args) {
        //实例化线程任务类
        RunnableThreadTest task1 = new RunnableThreadTest();
        for (int i = 0; i < 5; i++) {
            //创建线程对象,并将任务提交给线程执行;
            new Thread(task1).start();
        }
    }

1.3、实现Callable接口

    前面的两种方式实现接口Runnable和继承Thread类我们发现都没有返回值,很多时候我们是需要得到任务执行后的一个反馈的,所以需要其中执行得到异常和返回值,这里Callable接口就为我们提供了这样的便利,通过实现Callable接口,然后借助FutureTask异步来获取Thread线程执行的结果。具体步骤:
   (1) 创建一个类实现Callable接口,实现call方法,可提供返回值;
   (2) 创建一个FutureTask,指定Callable对象,做为线程任务;
   (3) 创建线程,指定线程任务。
   (4) 启动线程;
Callable接口源码:是一个函数式接口:
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

PS: Callable中可以通过范型参数来指定线程的返回值类型。通过FutureTask的毁掉方法get()异步拿到线程的返回值。

实现实例:
public class CallableThreadTest {

    private static Logger log = LoggerFactory.getLogger(CallableThreadTest.class);


    /*实现Callable接口,允许有返回值*/
    public static class UserCall implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("i am implements Callable");
            return "CallResult~";
        }
    }

    public static void main(String[] args) {

        UserCall userCall = new UserCall();
        //第一步:创建一个FutureTask对象;
        FutureTask<String> futureTask = new FutureTask<>(userCall);

        //第二步:创建线程,指定线程任务;
        Thread callThread = new Thread(futureTask);

        //第三步:启动线程
        callThread.start();

        try {
            //第四步:通过get()阻塞获取线程的返回值;
            String result = futureTask.get();
            log.info("thread result:{}", result);
        } catch (Exception e) { //只有痛过get()才能获取异常结果通知;
            log.error("Exception:", e);
        }
    }
   
}

Runnable和Callable的区别:

  • 返回值:callable有返回值,runnable没有返回值;
  • 提交:callable和rannable用submit提交,runnable只能用executor提交;
  • 异常处理:
    • Runnable方法内没有抛出任何异常,所有的异常必须自己捕获处理;
    • callable定义了异常向外抛,所以可以在方法体外接受checked异常;

2、线程状态

    线程的从创建到销毁的生命周期内,有六种状态,此六种状态的转换关系如下图所示:
 
(1). 初始状态-NEW
    初始状态,线程被构建,但是还没有调用 start 方法
    Thread thread = new Thread(); //初始状态;
(2). 运行状态-RUNNABLE
运行状态:JAVA线程把操作系统中的 就绪运行两种状态统一称为 “运行中”;
  • 当被调度程序选中后,线程就获取了CPU的运行权限,获取了CPU执行时间分片后,处于运行中;
  • 失去了cpu的执行权的状态:
就绪状态-READY,就绪状态说明你有资格参与cpu的竞争,随时等待着被cpu调度执行。就绪状态包含以下情况:
  • 线程调用了start()方法;
  • 当前线程sleep()方法结束,其他线程join()结束,I/O等待结束,例如用户输入完毕;
  • 拿到某个线程拿到对象锁;
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
(3). 阻塞状态 BLOCKED
阻塞状态,表示线程进入等待状态,也就是线程 因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况:
  • 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前 线程放入到等待队列    
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁等待队列中;
  • 其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方 法,或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完 毕则线程恢复;
(4). 等待 WAITTING
    此状态的线程将会处于无限期的等待状态,不会被分配CPU执行时间,直到它们被notify()/notifyAll()/LockSupport.unpark()显式地唤醒;
(5). 超时等待 TIMED_WAITTING
    超时等待状态,此状态的线程不会被分配CPU执行时间,但是它们不必无限期等待被其他线程显示地唤醒,在达到time_out时间达后就会自动唤醒。
(6). 终止状态 TERMINATED
    当线程的run()方法完成时,或者主线程的main()方法完成时,表示当前的线程执行完毕,线程一旦终止了,就不能复生。
    在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

2.1、查看线程的状态

通过jstack就可以查看当前线程的运行状态:

king@bogon:~(master)$ jps  //查看所有java进程的命令;
17376 ThreadStatuTest     //java进程id
8020 NutstoreGUI
32110 Jps
king@bogon:~(master)$
king@bogon:~(master)$ jstack 17376 //查看当前进程的线程栈
2020-10-27 12:51:52
Full thread dump OpenJDK 64-Bit Server VM (25.152-b26 mixed mode):


"Attach Listener" #310 daemon prio=9 os_prio=31 tid=0x00007fb71fbb0800 nid=0x615f waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

从日志显示,当前线程处于RUNNABLE状态。

3、理解函数yield()/sleep()/wait()

Park()/unpark() Object.wait/notify() 不同
  • 顺序:park和unpark在调用 顺序上无序关心;
  • 永久阻塞:如果先调用Object.notify()/notifyAll(),Condition.notify()/notifyAll() 则会出现调用了wait的线程得不到唤醒的情况;
LockSupport. park() 相比于 Object.wait() 的优点
  1. 直观:以线程为操作对象,更加直观;LockSupport.park();/LockSupport.unpark(thread);
  2. 精确:通过使用 「Condition多路等待队列 + 同步队列」操作更精准,唤醒的时候对目标线程直接操作 (相比于Object里的唤醒方法:notify是随机唤醒一个,notifyAll 是唤醒所有的线程 );
Thread.sleep() / Thread.yield()Object.wait() 都是让线程进入休眠,有什么 区别呢?
sleep 方法  -底层使用park() 
当前线程调用此方法,即当前线程会从 “运行状态”进入到“休眠(阻塞)状态”-TIMED_WAITING状态,millis后线程自动苏醒进入就绪状态。
  1. 属于线程类,必须传入休眠的时间;
  2. 该方法抛出InterruptedException异常,是受检查异常,调用者必须处理;
  3. 通过调用sleep方法休眠的线程 不会释放锁;例如sleep(0)就是触发一次cpu的线程重新调度;
  4. 释放cpu资源;
yield() 法意味着“让步”:暂时放弃cpu权限。类似sleep(0),触发一次cpu的重新调度;
  • 1. 当前线程调用此方法,当前线程由“运行状态”变为“就绪状态”,让出cpu重新调度;
  • 2. 但是它不能保证其他线程一定能够执行,因为执行过yield的线程有可能被cpu再次执行,继续进入到“运行状态”,这也是不建议使用的原因;
  • 3. 执行yield的线程 不会释放锁,这是要注意的。
使用实例:在ConcurrentHashmap中第一put数据的时候,进行map的初始化,其他线程如果发现已经有线程在对map进行初始化,则会让出cpu的使用权,保证map只会被出初始化一次。
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            //如果有其他的线程在对map进行初始化,则让出其使用权;等待table初始化完成;
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

wait()特性:-底层使用park() 

    当前线程调用对象的object.wait(),释放对象监视器(对象锁)的所有权,进入等待队列,依靠notify()/notifyAll()唤醒或者timeout时间到自动唤醒,然后同步队列里竞争锁,只有同步队列的线程才有资格获取对象锁,只有当当前线程获取该对象监视器后才可以继续执行,synchronized + wait() 是一对。obj.notify()/notifyAll()唤醒在此对象监视器上等待的单个线程,由“等待队列”进入“同步队列”,唤醒的线程是任意的。notifyAll()会唤醒在此对象监视器上等待的所有线程。
  1. 每个java对象都隐式的继承了Object对象;提供了3个方法: wait()/notify()/notifyAll()方法唤醒;
  2. 必须获取对象锁Synchronized才能调用wait方法;
作用:
  • 1、释放锁;
  • 2、释放cpu资源;
  • 2、阻塞当前的线程,当前线程由“运行状态”-RUNNABLE 进入到“等待(阻塞) BLOCKED状态”,
Thread wait() sleep() 的区别?
总结一句话: 只有wait()方法才释放锁和锁定的资源及cpu资源,其他的除了cpu锁是宁死不放;
方法
状态
cpu资源
锁资源
yield()
 
RUNNABLE
“运行状态”进入到“就绪状态”
暂时放弃,但是可能再次获取
类似于sleep(0)
不释放
sleep()
TIMED_WAITING
运行状态”进入到“等待状态”;
sleep是属于Thread类的方法;
放弃cpu直到超市时间
不释放
wait()
WAITING
运行状态”进入到“等待(阻塞)状态”;
wait()是属于Object类的方法;
必须配合synchronized使用
放弃cpu直到唤醒
释放

4、join()实现线程可见性的原理

join的主要功能:让线程的执行结果让其他的线程可见;
问题思考:在java顺序性保证happen-before原则中,有一条就是join()原则,保证线程的执行结果可见,那么它是如何控制线程的执行顺序,实现结果可见的呢?
参考一段代码实例:
如何让A线程修改共享变量num后的值让其他线程B可见呢?
public class ThreadJoinNum {
    private static int num = 0;
    public static void main(String[] sure) throws InterruptedException {

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                num += 5; //如何让线程对num的修改结果对其他的线程可见;
            }
        });

        System.out.println(Thread.currentThread().getName() + " before threadA num:  "+ num);
        threadA.start();
        threadA.join();  //阻塞主线程,使线程A修改后的结果让其可见;
        System.out.println(Thread.currentThread().getName() + " after threadA num:  "+ num);

    }
}

分析:当不加threadA.join()的时候,threadA和main线程正常执行结果是不可预知的:因为main和threadA同时运行,num的结果可能是:0也可能是5。流程如下图所示:

当加入threadA.join()之后运行结果:
main before threadA num:  0
main after threadA num:  5

从结果来看,我们完美的实现了线程执行顺序的控制,那么它是如何实现的呢?

Join 实现原理:线程调用了 t.join() || t.join(long)之后, 本质还是通过同步机制wait()/notify() 让线程串行化;
  • 执行join的时候,通过获取threadA线程的实例锁,此时 调用主线程main会进入了阻塞wait(),暂时放弃了cpu执行权;
  • 直到 threadA线程运行完成销毁的时候调用notifyAll()通知主线程main,然后再继续执行;
  • 至此实现了threadA执行的结果被main线程可见;
具体流程如下图所示:

5、join源代码分析

一:阻塞的实现
public final void join() throws InterruptedException {
    join(0);
}
//获取线程对象实例,线程实例会比较特殊,在线程结束的时候会唤醒其他阻塞在该线程实例上的线程,notifyAll();
public final synchronized void join(long millis) {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {      //如果执行的是join(0)
        while (isAlive()) { //只要threadA线程存活就一直阻塞
            wait(0);        //调用了wait()方法阻塞调用此方法的main线程,直至threadA线程执行完成;
        }
    } else {                //如果执行的是join(time)
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay); //超时等待阻塞time后自动返回继续执行;
            now = System.currentTimeMillis() - base;
        }
    }
}

分析:从源码可以看出,线程在执行了join()方法后,只要线程是存活状态isAlive()就会一直阻塞其他线程的执行,或者是阻塞超时等待的时间就返回,当线程结束的时候就会调用notifyAll()唤醒其他阻塞的线程继续执行,让此线程的执行结果对其他线程可见。

6、问题思考

思考一: 线程Runnable和running状态的区别?
  • start()后线程处于:就绪Ready和Running状态,这两中状态都称之为Runnable状态,这两个状态取决于cpu的调度,主要是是否抢到cpu时间;
  • 因为cpu的使用是基于时间片抢占式调度的,并不是由程序决定的;
思考二: 为什么说线程切换比较消耗性能?
  • 1、线程的一次上下文切换都会导致用户态到内核态的切换,因为java的线程实现模型是用户线程和内核线程1:1的实现方式,挂起到唤醒的过程都是系统级别的切换,需要保存现场数据到重新加载数据,竞争cpu使用权;
  • 2、占中内核的内存空间-线程内局部变量的存储栈空间;
  • 3、所有该线程用到的cpu的指令和缓存都将失效;
思考三同步和异步、阻塞和非阻塞的区别?
同步和异步:关注的用户态和内核态是消息通信机制-同步还是异步:指的是用户线程和内核的交互方式,
  • 同步:循环死等-等待时长取决于被调用的服务端的性能;消息发出后等待回执;
  • 异步:调用后就返回,消息发出后不用同步等待回执;不等待,通过回掉/事件消息通知;Node.js
阻塞和非阻塞:  关注的是线程等待io操作结果时是否挂起阻塞,指用户线程调用内核线程做IO操作的方式是阻塞还是非阻塞.
  • 阻塞: 线程状态(Blocked park/wait/)程序完全放弃了cpu,等待结果的线程将被挂起;
  • 非阻塞: 线程状态(Runnable)不挂起,可以执行其他操作,也可以几分钟check一下,继续执行;
例如同步阻塞方式:例如客户端读取文件:当有io阻塞的时候,read线程会放弃cpu,一直阻塞等待服务器读文件直到完成,期间什么都干不了;

7、小结

     线程的执行由操作系统调度决定的,不是程序员决定的。 一个操作“时间上的先发生”,不代表这个操作是先行发生,代码的执行与代码的书写没有必然联系,但是遵循 串型一致性(as -if - serials)原则。
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值