1.几个基本的概念
进程(Process):
一个程序运行起来时在内存中开辟一段空间用来运行程序,这段空间包括heap、stack、data segment和code segment。例如,开一个QQ就表明开了一个QQ进程。
线程(Thread):
每个进程中都至少有一个线程。线程是指程序中代码运行时的运行路径,一个线程表示一条路径。例如QQ进程中,发送消息、接收消息、接收文件、发送文件等各种独立的功能都需要一个线程来执行。
进程和线程的区别:
从资源的角度来考虑,进程主要考虑的是CPU和内存,而线程主要考虑的是CPU的调度,某进程中的各个线程之间可以共享这个进程的很多资源。
从粒度粗细来考虑,进程的粒度较粗,进程上下文切换时消耗的CPU资源较多。线程的粒度要小的多,虽然线程也会切换,但因为共享进程的上下文,相比进程上下文切换而言,同进程内的线程切换时消耗的资源要小的多的多。在JAVA中,除了java运行时启动的JVM是一个进程,其他所有任务都以线程的方式执行,也就是说java应用程序是单进程的,甚至可以说没有进程的概念。
线程组(ThreadGroup):
线程组提供了一些批量管理线程的方法,因此通过将线程加入到线程组中,可以更方便的管理这些线程
线程的状态
就绪态、运行态、睡眠态。还可以分为存活和死亡,死亡表示线程结束,非死亡则存活,因此存活包含就绪、运行、睡眠。
java thread的运行周期中, 有几种状态, 在 java.lang.Thread.State 中有详细定义和说明:
NEW 状态是指线程刚创建, 尚未启动
RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等
WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
TIMED_WAITING 这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)
中断睡眠(interrupt):
将线程从睡眠态强制唤醒,唤醒后线程将进入就绪队列等待cpu调度。
并发操作:
多个线程同时操作一个资源。这会带来多线程安全问题,解决方法是使用线程同步。
线程同步:
让线程中的某些任务原子化,即要么全部执行完毕,要么不开始执行。通过互斥锁来实现同步,通过监视这个互斥锁是否被谁持有来决定是否从睡眠态转为就绪态(即从线程池中出去),也就是是否有资格去获取cpu的执行权。线程同步解决了线程安全的问题,但降低了程序的效率。
死锁:
线程全睡眠了无法被唤醒,导致程序卡死在某一处无法再执行下去。典型的是两个同步线程,线程1持有A锁,且等待B锁,但线程2持有B锁且等待A锁,这样的僵局会造成死锁。但需要注意的是,死锁并非都是因为僵局,只要两边的线程都无法继续向下执行代码(或者两边的线程池都无法被唤醒,这是等价的概念,因为锁等待也会让进程进入睡眠态),则都是死锁。
还需需要明确的一个关键点是:CPU对就绪队列中每个线程的调度是随机的(对我们人类来说),且分配的时间片也是随机的(对人类来说)。
2.创建线程的两种方法
继承Thread类
创建线程方式一:
- 继承Thread类(在java.lang包中),并重写该类的run()方法,其中run()方法即线程需要执行的任务代码。
- 然后new出这个类对象。这表示创建线程对象。
- 调用start()方法开启线程来执行任务(start()方法会调用run()以便执行任务)。
class MyThread extends Thread { String name; String gender; MyThread(String name,String gender){ this.name = name; this.gender = gender; } public void run(){ int i = 0; while(i<=20) { //除了主线程main,其余线程从0开始编号,currentThread()获取的是当前线程对象 System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender); i++; } } } public class CreateThread { public static void main(String[] args) { MyThread mt1 = new MyThread("malong","Male"); MyThread mt2 = new MyThread("Gaoxiao","Female"); mt1.start(); mt2.start(); System.out.println("main thread over"); } }
创建线程方式二:
- 实现Runnable接口,并重写run()方法。
- 创建子类对象。
- 创建Thread对象来创建线程对象,并将实现了Runnable接口的对象作为参数传递给Thread()构造方法。
- 调用start()方法开启线程来执行run()中的任务。
class MyThread implements Runnable { String name; String gender; MyThread(String name,String gender){ this.name = name; this.gender = gender; } public void run(){ int i = 0; while(i<=200) { System.out.println(Thread.currentThread().getName()+"-----"+i); i++; } } } public class CreateThread2 { public static void main(String[] args) { //创建子类对象 MyThread mt = new MyThread("malong","Male"); //创建线程对象 Thread th1 = new Thread(mt); Thread th2 = new Thread(mt); th1.start(); th2.start(); System.out.println("main thread over"); } }
这两种创建线程的方法,无疑第二种(实现Runnable接口)要好一些,因为第一种创建方法继承了Thread后就无法继承其他父类。