JAVA中的线程

并发与并行

并发:同一时间只有一个线程在运行。
并行:同一时间多个线程同时运行。
对于单核cpu来说,所有线程都是并发的,但是cpu执行的时候被分成了很多时间片,每个时间片执行时间是固定的,且切换速度很快,所以看上去线程是并行的。
而对于多核cpu来说是支持并行的,每个运算核心实际上处理线程的方式还是和单核一样,具体线程的调度有专门的调度算法(鄙人暂时跳过)。

线程与进程

线程:cpu执行的最小单位
进程:系统进行资源分配和调度的最小单位。进程包含线程,执行进程实际上就是执行该进程所包含的线程,同一个进程的线程共享该线程的上下文环境(即内存,显卡等资源)。所以执行一个进程的完整过程就是首先加载进程的上下文环境,再执行该进程所包含的线程,然后保存上下文环境。最后切换下一个进程。
一个进程中创建线程的数量是有限的。操作系统每创建一个进程会分配一定的内存空间(据说是2G,分配原理暂时跳过),而每个进程创建线程的时候同样会给每个线程预留空间,所以每个进程最大能创建线程数则由分配给每个线程的内存大小决定(默认是2M可以修改)。
对于操作系统来说线程模型分为内核线程和用户线程(不太理解),还有一个就是在内核线程基础之上建立的轻量级进程,每一个轻量级进程都对应一个内核线程(轻量级进程是内核线程的高度抽象)。linux服务器采用轻量级进程模型,前面说到了进程是调度的最小单位,而轻量级进程和内核线程属于一对一关系,那么如果使用轻量级进程模型linux只需要一个进程层面的调度器。其中linux调度器主要分为三类。SCHED_OTHER 分时调度策略(默认的) , SCHED_FIFO 实时调度策略(先到先服务) SCHED_RR 实时调度策略(时间片轮转)。
而Java程序都是运行在Java虚拟机上面的,虚拟机帮我们屏蔽了操作系统的差异(不同操作系统进程模型是不一样的)。一个java程序就是一个进程,java都是单进程多线程的。Jvm中实现了一个线程调度模型:抢占式调度模型或者协同式调度模型,根据这个为多线程分配cpu空间。但实际上Java线程通过映射到系统原生线程来执行的,所以线程调度最终还是取决于操作系统。在linux系统中java进程中的线程映射成为了轻量级进程用于抢夺cpu资源,而这些轻量级进程又共享一个java进程的上下文资源。
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务,守护线程最典型的应用就是GC(垃圾回收器)。这两种线程其实是没有什么区别的,唯一的区别就是Java虚拟机在所有“用户线程”都结束后就会退出。我们可以通过使用setDaemon()方法通过传递true作为参数,使线程成为一个守护线程。我们必须在启动线程之前调用一个线程的setDaemon()方法。否则,就会抛出一个java.lang.IllegalThreadStateException。同时可以使用isDaemon()方法来检查线程是否是守护线程。

使用Thread创建线程

先看看Thread是怎么创建一个线程并运行的,代码如下。

public class MyThread  {
    public static int number ;
    public static int count = 100;
    public static void main(String[] args){
        Thread1 thread1 = new Thread1();
        thread1.setName("线程1");
        thread1.start();    
    }
}
class Thread1 extends Thread{
    @Override
    public void run(){
        while (true){
            if(MyThread.count < 0 ){
                break;
            }else {
                MyThread.count--;
            }
            MyThread.number = 100-MyThread.count;
            System.out.println(Thread.currentThread().getName()+"得到了"+"第"+MyThread.number+"张车票");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

以上可知通过继承Thread创建一个线程实际上是调用的Thread类中的start()方法。而start()方法实际上又是调用的由native修饰的本地方法start0()去创建一个线程(java方法与本地方法以及java如何调用本地方法略)。此时该线程便处于运行状态,等待cpu的调度执行重写的run方法中的代码。如果主线程想要等到创建的线程执行完成之后在运行,可以使用Thread.join()方法。

使用Runnable创建线程

使用Runnable创建代码如下

public class MyRunnable {
    public static int number ;
    public static int count = 100;
    public  static void main(String[] args){
        Runnable1 runnable1 = new Runnable1();
        Thread thread1 = new Thread(runnable1);
        thread1.start();
    }
}
class Runnable1 implements Runnable{
    @Override
    public void run() {
        while(true){
            if(MyThread.count < 0){
                break;
            }else {
                MyThread.count--;
            }
            MyThread.number = 100-MyThread.count;
            System.out.println("线程"+Thread.currentThread().getName()+"得到了"+"第"+MyThread.number+"张车票");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

通过以上代码可知通过实现Runnable接口创建线程,实际上还是将Runnable接口实现类的实例对象传入Thread类对象中,由Thread类中的start方法创建线程,再等待cpu调度执行该对象的run方法。其实Thread实现的run方法也是Runnable接口中定义的,因为Thread也实现了Runnable接口。那么就是说Runnable接口中run方法是线程执行内容的载体,而Thread类中start方法则是用来创建线程的。那么既然原理都是一样的为什么还要提供两种实现方式呢?主要还是因为Thread是一个类,如果某个类想要创建线程继承了Thread类了之后就不能再继承其他父类了,所以Thread
类初始化的时候提供了传入Runnable接口实现类的实例对象的方法。

使用Callable创建线程

前面提到的通过Thread和Runnable创建线程都是异步执行的,并不能拿到线程的执行结果。而使用Callable创建线程则可以拿到线程的执行结果。通过Callable创建线程代码如下。

public class MyCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable1 callable1 = new Callable1();
        FutureTask futureTask = new FutureTask<>(callable1);
        Thread aa = new Thread(futureTask);
        aa.start();
        System.out.println(futureTask.get().toString());
    }
}
class Callable1 implements Callable{
    @Override
    public Object call() throws Exception {
        Date date = new Date();
        return date;
    }
}

其中FutureTask类实现了RunnableFuture接口,而RunnableFuture接口又继承了Runnable接口以及Future接口。所以FutureTask实例也可以用来初始化Thread类对象,并通过Thread类中start方法启动线程。线程启动之后会调用FutureTask中实现Runnable接口中的run()方法,run()方法再去调用Callable类对象中的call()方法并得到返回结果result,再将result封装进成员变量中,之后便可以通过get()方法得到返回值。FutureTask实现了Future接口中的方法如下。
boolean cancel(boolean mayInterruptIfRunning):取消该Future里面关联的Callable任务
V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
boolean isDone():若Callable任务完成,返回True
boolean isCancelled():如果在Callable任务正常完成前被取消,返回True。
其实FutureTask类中除了直接传Callable类对象来初始化之外,还可以通过传Runnable接口实现类类对象 + 返回类型 来初始化。在之后的内容中会讲到。

使用线程池创建线程

前面已经提到线程执行的两个主要接口:Runnable和Callable,以及创建线程的类Thread和得到线程执行结果的类FutureTask。其中Thread类以及Runnable接口是JDK中一开始就提供了的。而FutureTask,Future,Callable则是在JDK1.5开始提供的Executor框架的包concurrent下的。Executor框架是一个用于统一创建线程与运行线程的接口。Executor框架实现的就是线程池的功能。先看下如何用线程池创建线程。

public class MyThreadPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService1= new ThreadPoolExecutor(1,1,60L,TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
        executorService1.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        executorService1.shutdown();
        ExecutorService executorService2 = new ThreadPoolExecutor(1,1,60L,TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
        Future future = executorService2.submit(new Callable<Object>() {
            @Override
            public Object call()  {
                return new Date();
            }
        });
        System.out.println(future.get());
        executorService2.shutdown();
    }
}

关于使用ThreadPoolExecutor创建线程池源码没太看懂,之后再补上。大概处理逻辑是如果核心线程没满则创建一个核心线程。如果已经满了则存入队列中等待核心线程空闲再去运行。如果队列也满了则创建一个非核心线程运行,如果非核心线程也满了则执行饱和策略(共四种,也可以选择自己实现饱和策略)。而submit与execute的区别则在于submit可以返回一个future对象用于获取线程执行的结果。而对于Executors工具类提供的创建常用线程池的方法一般不推荐使用,可以了解。

线程的状态转换

众所周知线程一共五种状态:新生状态,就绪状态,运行状态,阻塞状态,死亡状态
新生状态:当一个线程对象被创建出来就是新生状态 new Thread()。此时线程还是为用户态线程。

就绪状态:当一个新生线程执行Thread.start()方法就会变成就绪状态,拥有了占用cpu(即被cpu调度的权利),线程由用户态转变为核心态。

运行态:线程占用cpu执行程序。可以通过yield()方法将线程变成就绪状态,等待cpu的下一次调用。

阻塞状态:阻塞状态共分为三种情况,三种情况都会引起线程上下文的切换。线程上下文切换分两种,第一种是上面提到的线程由运行态转换为就绪态,cpu需要保存上一个线程的执行数据。第二种则是马上要提到的三种阻塞状态,会引起用户态与核心态线程的切换。第一种同步阻塞:当前线程执行某段被synchronized关键字修饰的程序时,如果锁此时已经被其它程序获取,那么该线程会进入同步阻塞状态,线程由核心态切换会用户态,JVM会将此线程放入锁池中(lock pool)等待获取锁。 第二种等待阻塞:在synchronized修饰的方法中调用Object.wait()方法,此时线程会释放cpu的执行权限同时也会释放锁(如果有)的拥有权限,进入等待阻塞状态。当然此时线程也会由核心态转变成用户态。需要调用通过Object.notify()或者Object.notifyAll()方法,让该线程转为运行态。如果该线程还释放了锁的话,还需要进入锁池等待重新获取锁才能转为运行态,线程也会由用户态线程转变为核心态线程。第三种普通阻塞:线程调用了Thread.sleep()方法或者Thread.join()方法(其实也是通过wait()方法实现的)或者发出了I/O请求。此时线程会进入普通阻塞状态,当其它线程的join()方法执行完毕或者sleep()方法时间已到或者I/O处理完毕,会自动进入就绪状态,等待cpu的调度。当然线程核心态和用户态也会跟着转换。

死亡状态:线程运行完毕,进行销毁。

线程的销毁策略

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值