美文网首页
java多线程干货(一)

java多线程干货(一)

作者: 晏子小七 | 来源:发表于2018-03-19 17:01 被阅读21次

    线程与进程

    1 线程:进程中负责程序执行的执行单元
    线程本身依靠程序进行运行
    线程是程序中的顺序控制流,只能使用分配给程序的资源和环境

    2 进程:执行中的程序
    一个进程至少包含一个线程

    3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程

    4 多线程:在一个程序中运行多个任务
    目的是更好地使用CPU资源
    线程的实现
    继承Thread类

    在java.lang包中定义, 继承Thread类必须重写run()方法:
    class MyThread extends Thread{
    private static int num = 0;

    public MyThread(){
        num++;
    }
    
    @Override
    public void run() {
        System.out.println("主动创建的第"+num+"个线程");
    }
    

    }

    start()方法调用和run()方法调用的区别,请看下面一个例子:
    public class TestThread {

    /*//创建MyThread线程类后,创建线程对象,然后通过调用start()方法去启动线程
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
    }*/
    
    
    //为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:
        public static void main(String[] args)  {
            System.out.println("主线程ID:"+Thread.currentThread().getId());
            Mythread thread1 = new Mythread("thread1");
            thread1.start();
            Mythread thread2 = new Mythread("thread2");
            thread2.run();
        }
    

    }

    结果: TIM图片20180315111735.png
    从输出结果可以得出以下结论:

    1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;

    2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

    实现Runnable接口

    在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。
    下面是一个例子:
    public class Test {
    public static void main(String[] args) {
    System.out.println("主线程ID:"+Thread.currentThread().getId());
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    }
    }
    class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子线程ID:"+Thread.currentThread().getId());
    }
    

    }

    Executor框架:
    使用ExecutorService、Callable、Future实现有返回结果的多线程:
    并发编程的一种编程方式是把任务拆分为一些列的小任务,即Runnable,然后在提交给一个Executor执行,Executor.execute(Runnalbe) 。Executor在执行时使用内部的线程池完成操作。
    一、创建线程池
    Executors类,提供了一系列工程方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
    public static ExecutorService newFixedThreadPool(int nThreads)

    创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()

    创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    public static ExecutorService newSingleThreadExecutor()

    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
    ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。
    可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
    @SuppressWarnings("unchecked")
    public class TestExecutor {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        System.out.println("---程序开始执行---");
        Date date1 = new Date();
        
        int taskSize = 5;
        //创建线程池
        ExecutorService pool =  Executors.newFixedThreadPool(taskSize);
        //创建多个有返回的任务
        List<Future> list = new ArrayList<Future>();
        for(int i=0;i<taskSize;i++){
            Callable c = new MyCallable(i+" "); 
            //执行任务并获取Future对象
            Future f = pool.submit(c);
            System.out.println("..."+f.get().toString());
            list.add(f);
        }
        //关闭线程池
        pool.shutdown();
        //获取所有并发任务的运行结果
        for(Future f : list){
            System.out.println("///"+f.get().toString());
        }
        Date date2 = new Date();
        System.out.println("----程序执行结束----,运行时间为:"+(date2.getTime()-date1.getTime())+"毫秒");
    }
    

    }
    class MyCallable implements Callable<Object>{
    private String taskNum;
    MyCallable(String taskNum) {
    this.taskNum = taskNum;
    }
    public Object call() throws Exception{
    System.out.println(">>>"+taskNum+"任务启动");
    Date dateTmp1 = new Date();
    Thread.sleep(1000);
    Date dateTmp2 = new Date();
    long time = dateTmp2.getTime()-dateTmp1.getTime();
    System.out.println(">>>"+taskNum+"任务终止");
    return taskNum+"任务返回运行结果,当前任务时间:"+time+"毫秒";
    }
    }
    代码说明:
    上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
    public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。
    ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

    线程的状态:

    * 创建(new)状态: 准备好了一个多线程的对象
    * 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
    * 运行(running)状态: 执行run()方法
    * 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
    * 终止(dead)状态: 线程销毁
    

    当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

    当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

    线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

    当由于突然中断或者子任务执行完毕,线程就会被消亡。
    注:sleep和wait的区别:
    1.sleep是Thread类的方法,wait是Object类中定义的方法。
    2.Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。
    3.Thread.sleep和Object.wait都会暂停当前的线程.OS会将执行时间分配给其他线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能重新获得CPU执行时间。
    何为上下文切换?
    对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
    由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
    说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。


    TIM图片20180319164935.png

    静态方法
    currentThread()方法

    currentThread()方法可以返回代码段正在被哪个线程调用的信息。
    public class Run1{
    public static void main(String[] args){
    System.out.println(Thread.currentThread().getName());
    }
    }
    sleep()方法

    方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

    sleep方法有两个重载版本:
    sleep(long millis) //参数为毫秒
    sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
    sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
    但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
    注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
    yield()方法

    调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

    注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

    对象方法
    start()方法

    start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
    run()方法

    run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
    getId()

    getId()的作用是取得线程的唯一标识
    Thread t= Thread.currentThread();
    System.out.println(t.getName()+" "+t.getId());
    isAlive()方法

    方法isAlive()的功能是判断当前线程是否处于活动状态:
    public class MyThread extends Thread{
    @Override
    public void run() {
    System.out.println("run="+this.isAlive());
    }
    }
    public class RunTest {
    public static void main(String[] args) throws InterruptedException {
    MyThread myThread=new MyThread();
    System.out.println("begin =="+myThread.isAlive());
    myThread.start();
    System.out.println("end =="+myThread.isAlive());
    }
    }
    join()方法

    在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。
    public class Thread4 extends Thread{
    public Thread4(String name) {
    super(name);
    }
    public void run() {
    for (int i = 0; i < 5; i++) {
    System.out.println(getName() + " " + i);
    }
    }
    public static void main(String[] args) throws InterruptedException {
    // 启动子进程
    new Thread4("new thread").start();
    for (int i = 0; i < 10; i++) {
    if (i == 5) {
    Thread4 th = new Thread4("joined thread");
    th.start();
    th.join();
    }
    System.out.println(Thread.currentThread().getName() + " " + i);
    }
    }
    }
    getName和setName

    用来得到或者设置线程名称。
    getPriority和setPriority

    用来获取和设置线程优先级。
    setDaemon和isDaemon

    用来设置线程是否成为守护线程和判断线程是否是守护线程。

    守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
    同步与死锁

    同步代码块
    在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块
    同步代码块格式 :synchronized(同步对象){
                                     需要同步的代码块;
                                    }

    相关文章

      网友评论

          本文标题:java多线程干货(一)

          本文链接:https://www.haomeiwen.com/subject/tvocqftx.html