并发第一弹 Java多线程核心基础

本文目录

  1. Java线程生命周期

  2. 什么是用户态与内核态?

  3. 终止线程的方式有哪些?

  4. sleep方法与wait方法的区别?

  5. 守护线程Daemon

  6. 线程基本方法

  7. 多线程最佳实践


Java线程生命周期


创建线程的方式:
  1. 继承Thread类

  2. 实现Runable接口

  3. 实现Callable接口

  4. lambda表达式

  5. 线程池


程序、进程、线程的区别:

  • 程序是静态的磁盘上的文件

  • 进程是运行起来的

  • 线程是一个程序里面不同的执行路径


Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。这6种状态分别是:

1、新建(New):创建后尚未启动的线程处于这种状态。

2、运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。

3、无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。以下方法会让线程陷入无限期的等待状态:

  • 没有设置Timeout参数的Object::wait()方法

  • 没有设置Timeout参数的Thread::join()方法

  • LockSupport::park()方法;

以下方法会主动唤醒线程:

  • Object::notify()

  • Object::notifyAll()

  • LockSupport::unpark()

4、限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:

  • Thread::sleep()方法;

  • 设置了Timeout参数的Object::wait()方法;

  • 设置了Timeout参数的Thread::join()方法;

  • LockSupport::parkNanos()方法;

  • LockSupport::parkUntil()方法;

5、阻塞(Blocked):阻塞状态与等待状态的区别是,阻塞状态在等待着获取到一个锁,即当前是无锁状态;而等待状态的前提是已经获取到锁了,然后调用wait、sleep等方法进入等待状态(注意:调用wait方法进入等待状态后会释放锁,调用sleep方法则不会释放锁)。

6、结束(Terminated):已终止线程的线程状态,线程已经结束执行


线程状态转换如下图所示。


并发第一弹 Java多线程核心基础


并发与并行区别:

  • 并行是真正的同时运行,前提条件是有多个CPU;

  • 并发是指一个时间段内有多个程序同时运行,并发不一定是并行的,假如只有一个CPU,你同时进行听音乐和打游戏,这两个程序看起来是在并行运行,但实际上是CPU通过时间片分片调度顺序执行,这两个程序是并行运行的关系;

Thread::start方法能多次调用吗?

  • 不能,一个线程只能运行一次,如果多次调用start则会抛出java.lang.IllegalThreadStateException;




什么是用户态与内核态?


操作系统需要两种CPU状态:

  • 内核态(Kernel Mode):运行操作系统程序,操作硬件;

  • 用户态(User Mode):运行用户程序;


指令划分:

  • 特权指令:只能由操作系统使用、用户程序不能使用的指令。比如:启动I/O 、内存清零、修改程序状态字、设置时钟、允许/禁止终端、停机等;

  • 非特权指令:用户程序可以使用的指令。比如:控制转移、算数运算、取数指令、访管指令(使用户程序从用户态陷入内核态)等;


特权级别:

  • 特权环:R0、R1、R2和R3;

  • R0相当于内核态,R3相当于用户态;

  • 不同级别能够运行不同的指令集合;


CPU状态之间的转换

  • 用户态->内核态:唯一途径是通过中断、异常、陷入机制(访管指令);

  • 内核态->用户态:设置程序状态字PSW;


内核态与用户态的区别:

  • 内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态,这是最低特权级,大部分用户直接面对的程序都是运行在用户态,占有的处理器是可被抢占的

  • 当程序运行在0级特权级上时,就可以称之为运行在内核态,所占有的处理器是不允许被抢占的

  • 运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当运行在用户态的程序需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件);


用户态切换到内核态的三种情况:

  • 系统调用:这是用户态进程主动要求切换到内核态的一种方式

  • 异常:异常会触发由当前运行进程切换到处理此异常的内核相关程序中缺页异

  • 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等;


这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。


终止线程的方式有哪些?

  • 正常运行结束

  • 使用退出标志

  • 使用interrupt()方法来中断线程

  • stop方法强制结束



使用退出标志


// 这里的exit是自定义的变量
while(!exit){
    //do something
}


使用退出标志中断线程的例子如下所示:

public class ThreadStopTest {
    public static boolean exit = false;
    public static void main(String[] args) {
        //这个线程的作用是每3秒打印出:helloworld
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!exit){
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("helloworld");
                }
            }
        }).start();
        //这个线程的作用是,10秒后停止上面那个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                exit = true;
            }
        }).start();
    }
}


 
使用interrupt()方法来中断线程

分两种情况:
  • 线程处于等待或阻塞状态:当调用线程的interrupt()方法时,会抛出InterruptException异常,通过代码捕获该异常,如果捕获之后不做任何事,则后面的代码仍然会执行。因此捕获异常后应使用break跳出循环或直接使用return,从而结束这个线程的执行;


public class StopThreadTest implements Runnable{ 
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new StopThreadTest());
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }
    
    @Override    
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
            System.out.println("hello");
        }
    }
}



  • 线程未等待且未阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理;


public class StopThreadTest implements Runnable{ 
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new StopThreadTest());
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }
    @Override    
    public void run() {
        while(!Thread.currentThread().isInterrupted()){
            System.out.println("hello");
        }
    }
}


stop方法强制结束

程序中可以直接使用Thread::stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生数据不一致性、死锁。


sleep方法与wait方法的区别?


对于sleep()方法,我们首先要知道该方法是属于Thread类中的,而wait()方法,则是属于Object类中的。

sleep()方法导致程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,即调用sleep()方法,线程不会释放锁

而当调用wait()方法的时候,线程会释放对象锁,然后进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后,该线程才进入准备获取对象锁的状态;由于wait方法会释放锁,因此一般用在同步方法或同步代码块中

 

守护线程Daemon


守护线程也称服务线程,他是后台线程,它有一个特性,即为非守护线程提供服务,在没有非守护线程可服务时会自动离开。

垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。

守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。


守护线程的示例代码如下所示:

public class StopThreadTest implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new StopThreadTest());
        Thread t2 = new Thread(new StopThreadTest());
        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();
        System.out.println(t1.isDaemon());
        System.out.println(t2.isDaemon());
        System.out.println(Thread.currentThread().isDaemon());
    }
    @Override
    public void run()
{
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId());
        }
    }
}


以上程序输出结果:

true
true
false



如果t1和t2都设置为守护线程,那么当main线程结束后(main线程是非守护线程),系统中就没有非守护线程了,所以t1和t2线程都会自动退出,不会一直打印各自的线程ID。


 

线程基本方法


线程让步(yield)

yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。


notify与notifyAll

Object::notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒在此监视器上等待的所有线程。


为什么wait, notify 和 notifyAll这些方法不在thread类里面

因为JAVA提供的锁是对象级的而不是线程级的,wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中。

 
interrupted和 isInterrupted方法的区别

首先,打断一个线程调用interrupt方法,interrupted和 isInterrupted都返回boolean值(是否中断),interrupted和 isInterrupted的主要区别是前者会将中断状态清除而后者不会。

任何抛出了InterruptedException异常的方法都会将中断状态清零,所以我们测试两者区别的时候,不要用Thread.sleep


测试代码如下所示:

Thread t = new Thread(() -> {
    //不要用Thread.sleep
    /* try {
    Thread.sleep(10000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }*/


    int i = 0;
    for(;;){
        i++;
        if(i>99999){
            break;
        }
    }
    //System.out.println(Thread.interrupted());
    //System.out.println(Thread.interrupted());
});
t.start();
t.interrupt();
System.out.println(t.isInterrupted());
System.out.println(t.isInterrupted());


有三个线程T1,T2,T3,怎么确保它们按顺序执行?

当t1和t2两个线程同时运行此时t1在某个地方调用了t2.join,那么t1会等t2运行结束后再跑自己的代码。在t1线程里调用t1.join是没有意义的。

Thread::join也叫线程插队,示例代码如下所示。


Thread t3 = new Thread(()->{
  for (int i = 1; i < 10000; i++) {
    System.out.println(Thread.currentThread().getName()+"--"+i);
  }
},"线程3号");

Thread t2 = new Thread(()->{
  t3.join();
  for (int i = 1; i < 10000; i++) {
    System.out.println(Thread.currentThread().getName()+"--"+i);
  }
},"线程2号");

Thread t1 = new Thread(()->{
  t2.join();
  for (int i = 1; i < 10000; i++) {
    System.out.println(Thread.currentThread().getName()+"--"+i);
  }
},"线程1号");

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



多线程最佳实践

  • 给你的线程起个有意义的名字:这样可以方便找bug或追踪;

  • 不用锁或缩小同步的范围:锁花费的代价高昂且上下文切换更耗费时间空间

  • 多用同步类少用wait 和 notify:CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制;

  • 多用并发集合少用同步集合:比如首先想到用ConcurrentHashMap它内部细分了若干个小的HashMap,称之为段(Segment)。默认情况下一个ConcurrentHashMap被进一步细分为16个段,即就是锁的并发度;



原文始发于微信公众号(初心JAVA):并发第一弹 Java多线程核心基础

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35562.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!