线程基础
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈
java中线程的实现方式:
- 继承Thread类并重写其run方法(Thread类也实现了Runnable接口,并提供了默认实现),调用Thread类的实例方法start()启动线程
- 实现Runnable接口(重写run()方法),传递Runnable对象给Thread对象,调用Thread类的实例方法start()启动线程
需注意的点:
- run方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出受检异常。run方法类似于单线程程序中的main方法,线程从run方法的第一条语句开始执行直到结束。
- start表示启动该线程,使其成为一条单独的执行流,背后,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。
- 如果直接调用run方法就是执行一个普通方法,并不会开辟新的线程。
- Thread有一个静态方法currentThread,可以返回当前执行的线程对象
线程基本属性和方法
id和name:
id是一个递增的整数,每创建一个线程就加一,name的默认值是"Thread-"后跟一个编号。name通过构造方法或者实例方法setName(String name)设置
优先级(priority):
在Java中,线程的优先级从1到10,默认为5,可通过实例方法getPriority()和setPriority(int newPriority)查看或设置
需注意:这个优先级会被映射到操作系统中线程的优先级,不过,因为操作系统各不相同,不一定都是10个优先级,Java中不同的优先级可能会被映射到操作系统中相同的优先级,另外,优先级对操作系统而言更多的是一种建议和提示,而非强制,简单的说,在编程中,不要过于依赖优先级。
状态:
线程有个实例方法getState()返回枚举类型Thread.State的状态,如下:
public enum State { NEW,//没有调用start的线程状态为NEW RUNNABLE,//调用start后线程在执行run方法且没有阻塞时状态为RUNNABLE,不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件 BLOCKED,//线程被阻塞,位于锁等待队列在等待锁 WAITING,//线程被阻塞,位于条件等待队列在等待被notify TIMED_WAITING,//线程被阻塞,位于条件等待队列等待被notify或者超时 TERMINATED;//线程运行结束后状态为TERMINATED }
可以调用Thread的实例方法isAlive(),查看线程是否活着:线程被启动后,run方法运行结束前,返回值都是true。
是否daemo线程
可通过实例方法setDaemon(boolean on)和isDaemon()设置或查看线程是否是守护线程。
守护线程:一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。所以当整个程序中剩下的都是daemo线程的时候,程序就会退出。java中负责垃圾回收的线程就是典型的守护线程
sleep方法
Thread的一个静态方法,调用sleep(long millis)方法会让当前线程睡眠指定的时间,睡眠期间线程会让出cpu。可中断,会抛出InterruptedException
yield方法
Thread的一个静态方法,调用yield()方法是告诉操作系统的调度器自己可以让出cpu,但是调度器可能忽略
join方法
Thread有一个实例方法join(),可以让调用join的线程等待该线程结束.它有个变体,可传入最长等待时间
共享内存及问题
共享内存
每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但是不同线程之间可以共享堆区和方法区的内容,比如可以共享同一个对象或类的静态属性
竞态条件
竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
举例:对非原子操作i=i+1来讲,当一个线程取得i原来的值(假设为1)+1后还没来得及再次赋给i,就被另一个线程取到了i未+1之前的值(1)再次进行+1,最后导致了我们期望结果为3但却是2的问题。
内存可见性
在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,而稍后才会同步更新到内存中。在单线程的程序中,这一般不是个问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是个严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读
(此时volatile可以登场了,当然锁也可以)
线程的优点及成本
优点
- 充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以利用多CPU的计算能力。
- 充分利用硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
- 在用户界面(GUI)应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,否则,如果所有事情都是一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
- 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种请求,以及各种网络和文件IO事件,建模和编写程序要容易的多
成本
创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。
当线程足够多的时候,操作系统忙于上下文切换(分配cpu时间片,保存程序计数器和栈中的数据到内存等),这个切换不仅耗时,而且使CPU中的很多缓存失效。