007-Java线程

Java知识点总结系列目录

1. 线程概念

  • 进程
    进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间)。是操作系统分配资源的最小单位。比如打开一个浏览器它就是一个进程。
  • 线程
    线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个进程内部可以有多个线程。
  • 线程特点
    1)线程是轻量级的进程
    2)线程没有独立的地址空间(内存空间)
    3)线程是由进程创建的(寄生在进程)
    4)一个进程可以拥有多个线程,交替占用同一个CPU资源(单核情况下是交替执行的,多核情况下可以多个CPU并行执行)

2. 线程状态及常用操作
007-Java线程_第1张图片
线程状态

  • 创建(New)
    线程对象被创建后,就进入了新创建状态。
  • 就绪(Runnable)
    线程对象调用start方法后进入就绪状态,此时该线程随时可能被CPU调度执行
  • 运行(Running)
    线程获取CPU权限进行执行。线程只能从就绪状态进入到运行状态。
  • 阻塞(Blocked)
    线程因为某种原因放弃CPU使用权,暂时停止运行。阻塞状态分为以下三种情况
    1)等待阻塞:通过调用线程的wait()方法,让线程等待其他工作先完成
    2)同步锁阻塞:线程未抢占到同步锁将进入同步阻塞状态
    3)其他阻塞:过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态
  • 死亡(Dead)
    线程运行完或异常退出,该线程结束生命周期。

线程操作方法

基本原则(高能):线程(Thread)方法分为静态方法和非静态方法,非静态方法作用在调用者上,静态方法作用在当前运行的线程上。有了这个基本原则理解以下方法将会事半功倍。

1)start:Thread非静态方法。启动一个新线程,线程状态从创建状态变为就绪状态。当线程有机会获取CPU资源时,开始执行内部run方法中的代码。

2)sleep:Thread静态方法。让当前正在运行的线程休眠(挂起)一段指定的时间,线程状态变为阻塞状态。当指定的休眠时间完成后被唤醒进行就绪状态(注意不是运行状态,需要抢占CPU资源后才能被运行)。此方法为Thread类的静态方法,提供的方法参数有毫秒+纳秒的方式,在实际使用中可以使用TimeUnit工具类中的方法,在参数传递时更加的方便。可以思考sleep(0)的意义。

3)join:Thread非静态方法。让当前正在运行的线程阻塞,等待调用join方法的线程执行完毕。简单的可以理解为让调用join方法的线程进行插队先运行完毕。这个方法也可以直接传入一个时间值,表示允许插队线程执行的超时时间。此方法内部是通过对象的wait方法实现的,一般用来控制多线程的顺序执行。比如,在线程A中调用线程B对象的join方法,线程A将被阻塞直到线程B执行完毕,然后将线程A唤醒。

	public static void main(String[] args) {
        Thread b = new Thread(()->{
            System.out.println("thread "+Thread.currentThread().getName()+" start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread "+Thread.currentThread().getName()+" over");


        }, "threadB");

        Thread a = new Thread(()->{
            System.out.println("thread "+Thread.currentThread().getName()+" start");
            try {
                b.start();
                b.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread "+Thread.currentThread().getName()+" over");
        }, "threadA");

        a.start();
    }

如上面代码示例,main线程中启动线程a。在运行线程a时,当前运行线程为线程a。在线程a内部启动线程b并调用线程b对象的join方法,线程b插入运行,线程a被阻塞,线程a等待线程b执行完毕后再被唤醒继续执行。
代码运行结果为:

thread threadA start
thread threadB start
thread threadB over
thread threadA over

4)wait:Object非静态方法。通过讲解join方法,我们知道join是通过wait方法来实现的。join方法是调用join方法的线程循环判断该线程是否存活,存活则一直wait,线程死亡则退出。wait方法是属于Object对象的方法,如对象a通过a.wait()来调用,当前正在运行的线程(执行a.wait()代码的线程)将被阻塞等待,并且将会释放持有对象a的锁。当其他线程调用对象a的notify或者notifyAll方法时,才会将等待在对象a上的线程唤醒到就绪状态。该方法必须在同步代码(同步方法或者同步代码块)中调用,不然会抛出IllegalMonitorStateException异常。

5)notify/notifyAll:Object非静态方法。唤醒当前对象上的等待线程;notify()是随机唤醒单个线程,而notifyAll()是唤醒所有的线程。和wait方法一样,notify和notifyAll方法都是属于Object对象的方法,必须在同步代码中调用。

6)interrupt/interrupted/isInterrupted: interrupt()为Thread非静态方法,该方法设置调用者的线程状态为中断状态,但并不是真正停止了该线程了运行,业务代码可以根据线程中断状态来做业务处理;isInterrupted()Thread非静态方法,检测线程中断状态,返回对应的布尔值;interrupted()为Thread静态方法,测试当前线程(currentThread)的中断状态并清除该状态,所以如果当第一次调用interrupted()返回true时,第二次调用返回false。

7)yield: yield()方法为Thread静态方法,作用是将当前运行的线程设置为就绪状态,让该线程重新去竞争CPU的调度,可以同时setPriority()方法来设置线程的优先级,优先级高的线程将有更大的几率抢占CPU的时间片。

3. 创建线程的几种方式

  • Runnable接口

    将实现了Runnable接口的对象传入Thread构造方法,创建线程,代码示例如下

    public class ThreadCreationTest {
    
        public static void main(String[] args) {
            new Thread(new ThreadImplementRunnable()).start();
        }
    
        static class ThreadImplementRunnable implements Runnable {
            @Override
            public void run() {
                System.out.println("Create thread by implement Runnable");
            }
        }
    }
    
    
  • 继承Thread类

    通过继承Thread类创建线程,示例代码如下

    public class ThreadCreationTest {
    
        public static void main(String[] args) {
            new ThreadExtend().start();
        }
    
        static class ThreadExtend extends Thread {
            public void run() {
                System.out.println("Create thread by extends Thread");
            }
        }
    }
    
    
  • Callable接口

    通过将实现了Callable接口的对象构建FutureTask对象,再将FutureTask对象通过Thread的构造方法创建线程。这种方式可以实现异步请求,示例代码如下

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    import java.util.concurrent.TimeUnit;
    
    public class ThreadCreationTest {
    
        public static void main(String[] args) {
            FutureTask<String> task = new FutureTask<>(new CallableImplement());
            new Thread(task).start();
    
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(task.isDone()){
                try {
                    System.out.println(task.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
    
        static class CallableImplement implements Callable<String> {
            public String call(){
                return "result";
            }
        }
    
    }
    
    
  • 线程池

    jdk1.5以后可以通过java的原生API先创建线程池,然后通过线程池来执行实现了Runnable接口的对象的方式来创建和执行线程了。JUC(java.util.concurrent)包中提供了线程池的工具类Executors来帮助开发者创建不同的线程池。

    1)通过Executors.newSingleThreadExecutor()的方式创建具有单一线程的线程池

    2)通过Executors.newFixedThreadPool(n)的方式来创建具有固定数量的线程的线程池

    3)通过Executors.newCachedThreadPool(n)的方式来创建具有可变数量的线程的线程池

    4)上述几种工具类Executors提供的创建线程池的方式分别都有些缺陷,比如最大线程数过大,等候队列太大导致一些问题的出现。实际开发中我们可以通过new ThreadPoolExecutor()的方式创建线程池(通过源码我们可以发现Executors提供的几种创建线程池的方式底层还是通过ThreadPoolExecutor来创建的),根据实际环境来指定具体的参数。示例代码如下

    		//ExecutorService executorService = Executors.newSingleThreadExecutor();    //方式1
            //ExecutorService executorService = Executors.newFixedThreadPool(2);        //方式2
            //ExecutorService executorService = Executors.newCachedThreadPool();        //方式3
            ExecutorService executorService = new ThreadPoolExecutor(
                    2,                              			//最小核心数
                    5,                          				//最大线程数
                    1,                             				//最大-最小 空闲保持时间
                    TimeUnit.SECONDS,   						//时间单位
                    new LinkedBlockingQueue<>(3),   			//等候队列
                    Executors.defaultThreadFactory(),			//默认线程创建工厂
                    new ThreadPoolExecutor.CallerRunsPolicy()   //当加入线程池的线程数大于线程池最大线程数加等候队列时采用的拒绝策略
            );                                                                          //方式4
    
            try {
                for (int i = 0; i < 10; i++) {
                    executorService.execute(()->{
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName());
                    });
                }
            } finally {
                executorService.shutdown();
            }
    

    四种方式可以通过输出的线程名字来看看具体参数的实际意义。实际开发中多用方式4来创建线程池, 最大线程数可以根据服务器的CPU核数和业务类型进行设置,其他参数也是根据业务情况进行优化设置。

    以上代码测试结果如下

    pool-1-thread-4
    main
    pool-1-thread-3
    pool-1-thread-5
    pool-1-thread-1
    pool-1-thread-2
    pool-1-thread-4
    pool-1-thread-5
    main
    pool-1-thread-3
    

    代码中设置的最大线程数是5,等候队列为3,拒绝策略为扔回调用者(这里是main线程)执行。我们加入了10个线程,所以当最大线程数加上等候队列数为8不够10个线程执行的时候,有两个线程被main线程执行了。当然这个执行结果并不一定是这样的,有可能只有一个线程被丢回main线程执行了,也有可能全部被线程池执行完毕,和CPU的运算速度有关。

你可能感兴趣的:(Java知识点总结系列,java,线程,线程池)