Java并发编程2:线程(Thread)

1.什么是线程

线程是操作系统的调度的最小单元,也叫轻量级进程,通常一个程序就是一个进程,一个进程可以创建多个线程。每个线程都有自己独立的程序计数器、栈、局部变量,同时可以访问共享的内存变量。CPU通过对这些线程高速切换,给人一种多个线程是在同时运行的错觉,实际上单个CPU同时只有一个线程在运行。

public class Test {

    public static void main(String[] args) {
        System.out.println("Test");
    }
}

我们都知道一个Java程序从main()开始执行,这样一个程序看似没有任何线程相关的操作,但实际上它也是一个多线程的程序,因为执行main()的是一个名称为main的线程,并且还有GC线程等线程。

2.线程的上下文切换

CPU通过给各个线程分配CPU时间片并进行高速切换来实现多线程,时间片就是指的CPU分配给各个线程的时间,通常几十毫秒,而高速切换就是接下来说的线程的上下文切换。
多线程条件下,当前线程执行完时间片后会执行其他线程的时间片(大多数情况下不会一直给一个线程分配时间片),在切换前需要把当前线程的执行状态保存下来,在该线程下一次分配到时间片后将保存的执行状态读取出来继续执行,这就是一次上下文切换
更好理解的例子是我们写代码写得正高兴时,被产品经理打断了,吵完扯淡的需求后,都得花一些时间从才能继续刚才的思路写下去。但是这样的过程是会耗费更多的精力,对CPU来说也是一样的道理,不停地上下文切换会消耗资源,降低多线程的执行速度。

所以多线程不一定都会加快程序运行速度,可能会减慢程序运行速度,我们需要根据实际项目需求来判断如何使用线程,对并发编程的深入理解能帮助我们得出正确的方案。
以下就是一个线程上下文切换相关的简单例子。

public class Test {
    private static final long COUNT = 10000*10000;

    public static void main(String[] args) {
        final long concurrent = concurrent();
        final long serial = serial();
        System.out.println("concurrent:" + concurrent());
        System.out.println("serial:" + serial());
        if (concurrent > serial) {
            System.out.println("串行执行更快");
        } else if (concurrent == serial) {
            System.out.println("一样快");
        } else {
            System.out.println("并发快");
        }
    }

    private static void task() {
        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a += 2;
        }
    }

    private static long concurrent() {
        long start = System.nanoTime();
        Thread thread = new Thread() {
            @Override
            public void run() {
                super.run();
                task();
            }
        };
        thread.start();
        task();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return System.nanoTime() - start;
    }

    private static long serial() {
        long start = System.nanoTime();
        task();
        task();
        return System.nanoTime() - start;
    }
}

运行程序后会发现当循环次数小的情况下串行速度更快,循环次数足够大的情况并行速度更快,循环次数小串行快的原因就是线程创建和线程上下文切换开销。

3.创建一个线程

创建一个线程有两种方式,一种是直接重写创建的Thread对象的run方法,二是在创建Thread的时候传入一个Runnable。

public class ThreadDemo {
    public static void main(String[] args) {
        // 重写run方法
        // 线程最好都传入线程名,方便定位线程问题
        Thread thread1 = new Thread("Thread1") {
            @Override
            public void run() {
                super.run();
                System.out.println(getName() + "的run方法" + "在执行");
            }
        };
        thread1.start();
        // 传入Runnable对象
        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("Runnable" + "在执行");
            }
        }, "Thread2") {
            @Override
            public void run() {
                // 注释1 super.run();
                System.out.println(getName() + "的run方法" + "在执行");
            }
        };
        thread2.start();
    }
}

以上是创建线程的两种方式,在这里对第二种创建Thread的方式多做一些说明。
运行上面的代码我们会发现Thread2中的Runnable的run方法没有调用,原因是我重写了Thread2的run方法,并且注释super.run(),将注释1去掉后,Thread2的Runnable的run方法就成功调用了。
查看Thread类的源代码
这里写图片描述
target是创建Thread传入的Runnable对象,通过源代码我们可以知道Thread的run方法默认实现是在targer非空时调用Runnable对象的run方法,所以加入我们使用第二种方式创建线程,就不要重写Thread的run方法,除非有特殊的需求。

4.线程优先级(Priority)

前文已经说过,线程的执行靠的是CPU分配时间片,所以线程分配的时间片的多少代表分配到的CPU资源的多少,而通过对Priority的设置就可以决定线程需要多或者少分配一些处理器资源。

线程Priority的范围是1~10,默认值是5,值越大优先级越高,对于频繁阻塞(睡眠或者IO操作)的线程设置高优先级,偏重计算(需要较多CPU时间)的线程设置低优先级,防止独占CPU。需要注意的是,并不能完全依赖优先级,因为有的操作系统会完全忽略优先级的设置。

Priority的设置有两种方式:
1.方法setPriority()
2.new Thread()时,默认使用创建该线程的线程的Priority,具体可以查看Thread源代码中的init方法。

5. 守护线程(Daemon)

守护线程是程序在后台提供一种通用服务的线程,在所有的非Daemon线程结束后,意味着程序应该结束了,然后会杀死所有的守护线程,无论这些守护线程是否正在运行。
由于守护线程随时都可能被程序结束,因此我们在使用守护线程应该注意一下几点:
1.绝对不能去访问固有资源,如数据库、文件等,因为该线程随时可能结束,会导致这些资源出现奇怪的错乱。
2.使用守护线程,不能通过使用finally来进行资源的回收等操作。所以使用守护线程我们应该谨慎,以免出现奇怪的错误。

public class DaemonDemo {

    public static void main(String[] args) {
        Thread thread = new Thread("TestDaemonThread") {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(getName() + " will run over");
            }
        };
        //将参数设置为false,就会看见run()中的输出
        thread.setDaemon(true);
        thread.start();
    }
}

以上是Daemon的使用方法,需要注意的是假如在设置Daemon属性为ture的线程中创建线程,那么新创建的线程也默认Daemon为true,具体原因可以查看Thread源码的init()方法。

6.执行线程

通常执行一个线程就是调用对应的run方法,run()的调用并不是创建一个Thread就可以了,创建一个线程后还需要调用start(),但是需要注意的是并不是调用了start()后就会同步调用run(),start()方法的作用是当前线程(即创建Thread的线程)告知Java虚拟机,假如线程规划器处于空闲状态,那就启动创建的线程(即调用run())。

7.暂停线程

暂停线程有两种方式:
1.使用suspend()方法,但是该方法已经被不建议使用,因为调用了该方法后线程不会释资源(比如锁),而是占着资源进入睡眠状态,这样容易引发死锁问题,并且也不适合于多线程编程。使用了suspend()暂停一个线程后可以使用resume()结束暂停,重新运行。
2.使用Object.wait()或者Lock类,具体怎么使用在后面的系列再展开。

8.终止线程

类似于suspend(),Thread类也提供了一个方法来终止线程,就是stop()方法,但是该方法也不建议使用,因为该方法会直接终止线程,不能保证一个线程的资源的正常释放,因此导致程序状态错乱。

安全的终止一个线程应该使用中断。中断相关的包括interrupt()、isInterrupt()、interrupted()、InterruptedException,下面通过代码来进行中断相关的用法。

public class InterruptDemo {
    public static void main(String[] args) {
        // test1();
        // test2();
        test3();
    }

    private static void test1() {
        Thread thread = new Thread("介绍inetrrupt()、interrupted()、isInterrupt() Demo") {
            @Override
            public void run() {
                super.run();
                // 中断线程,不要被该方法的名字所迷惑,Interrupt()方法并不能中断一个线程、
                // 调用后会将Thread的中断标志置为中断,并且会中断该线程挂起的方法(包括Object.wait()
                // 、Thread.Sleep()、Thread.join()、LockSupport.park())并抛出InterruptedException
                interrupt();
                // isInterrupt()的返回值为true表示Thread的中断标志已经置为中断
                System.out.println(getName() + " 线程");
                System.out.println("中断标志是否置为中断:" + isInterrupted());
                // interrupted()和isInterrupt()返回值含义一样,不同的是interrupted()会
                // 重置Thread的中断标志
                System.out.println("中断标志是否置为中断:" + interrupted());
                System.out.println("中断标志重置后.");
                System.out.println("中断标志是否置为中断:" + isInterrupted());
            }
        };
        thread.start();
    }

    private static void test2() {
        Thread thread = new Thread("介绍InterruptedException Demo") {
            @Override
            public void run() {
                super.run();
                System.out.println(getName() + " 线程");
                try {
                    Thread.sleep(1000);
                    // 调用挂起线程的方法都需要抛出 InterruptedException
                    // 通常在catch块中做挂起被中断的相关操作
                    // 需要注意的是抛出InterruptedException后
                    // 该Thread的中断标志会被重置.
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("中断标志是否置为中断:" + isInterrupted());
                }
            }
        };
        thread.start();
        // sleep一下,保证thread线程已经进入run()方法
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }

    private static void test3() {
        Thread thread = new Thread("介绍用中断终止线程 Demo") {
            @Override
            public void run() {
                super.run();
                System.out.println(getName() + " 线程");
                int i = 0;
                //其他线程通过调用Demo线程的interrupt()方法,改变中断标志位
                //从而终止该线程
                while (!isInterrupted()) {
                    i++;
                }
                System.out.println("线程终止,i=" + i);
            }
        };
        thread.start();
        // sleep一下,保证thread线程已经进入run()方法
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

上面代码相应地方的注释介绍了中断相关的基本用法。
使用中断终止一个线程是线程有机会再线程终止前进行资源释放,通常中断线程还要结合标志位进行判断,这里就仅仅是使用了对中断标志位的判断,不过原理都一样。

9.总结

以上对线程的一些基本使用方法做了介绍,并没有涉及高级的使用方法,对基础的深入理解有助于更好的理解高级的并发编程用法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值