1.了解什么是线程(what)
要知道线程,首先要知道什么是进程?
进程:正在执行的(进行的)程序,需要操作系统为其分配独立的内存地址空间。
线程:为了提高效率(目的),虚拟的把进程资源分配给线程,让在小的时间段内产生多个代码片段在运行.这些线程可以
共享同一块内存和系统资源等.特点:独立运行,但不占用系统资源.
2.如何创建线程(How)
1.继承类java.lang.Thread
2.实现接口java.lang.Runnable
Thread类代表线程类,它的两个最主要的方法是:
run()——包含线程运行时所执行的代码
start()——用于启动线程,运行run()中的代码片段
常见开发格式:
class ThreadTest extends Thread{
public void run(){
while(true){
//代码片段
....
//退出条件
if(条件){
break;
}
}
}
}
3.线程的状态
新建状态:用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内存
就绪状态:当一个线程创建了以后,其他的线程调用了它的start()方法,该线程就进入了就绪状态。处于这个状态的线程位于
可运行池 中,等待获得CPU的使用权
运行状态:处于这个状态的线程占用CPU,执行程序的代码
阻塞状态:当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态分为三种情况:
1、 位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,java虚拟机就回把线程放到这个对象
的等待池中
2、 位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程
占用,JVM就会把这个线程放到这个对象的琐池中。
3、 其它的阻塞状态:当前线程执行了sleep()方法,或者调用了其它线程的join()方法,或者发出了I/O请求时,就会进入这个
状态中。
死亡状态:当线程退出了run()方法,就进入了死亡状态,该线程结束了生命周期。 或者正常退出 或者遇到异常退出
Thread类的isAlive()方法判断一个线程是否活着,当线程处于死亡状态或者新建状态时,该方法返回false,在其余
的状态下,该方法返回true.
4.线程的调度
线程调度模型:分时调度模型和抢占式调度模型,JVM采用抢占式调度模型。
所谓的多线程的并发运行,其实是指宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
(线程的调度不是跨平台,它不仅取决于java虚拟机,它还依赖于操作系统)
如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下的办法之一
1、 调整各个线程的优先级
Thread类的setPriority(int)和getPriority()方法分别用来设置优先级和读取优先级。
如果希望程序能够移值到各个操作系统中,应该确保在设置线程的优先级时,只使用
MAX_PRIORITY、
NORM_PRIORITY、
MIN_PRIORITY
这3个优先级。
2、 让处于运行状态的线程调用Thread.sleep()方法
当线程在运行中执行了sleep()方法时,它就会放弃CPU,转到阻塞状态。
3、 让处于运行状态的线程调用Thread.yield()方法
当线程在运行中执行了Thread类的yield()静态方法时,如果此时具有相同优先级的其它线程处于就绪状态,
那么yield()方法将把当前运行的线程放到运行池中并使另一个线程运行。如果没有相同优先级的可运行线程,
则yield()方法什么也不做。
4、 让处于运行状态的线程调用另一个线程的join()方法
当前运行的线程可以调用另一个线程的 join()方法,当前运行的线程将转到阻塞状态,直到另一个线程运行结束,它才恢复运行。
补充:sleep()和yield()
sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU,把运行机会让给别的线程,
两者的区别在于:
1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;
yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;
当线程执行了yield()方法后,将转到就绪状态。
3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常
4、sleep()方法比yield()方法具有更好的移植性
5.线程的同步
什么是线程的同步?了解为什么会出现线程的同步?
多个线程在一段代码之间,可能出现异常情况,如:
public void run(){
if(i<10){
System.out.println(i++);
}
}
当i=9;如果线程1执行完判断条件后切换到线程2,线程2判断条件后执行后续的代码后i=10,这个时候线程切换到线程1,因为线程1已经
判断过,所以执行后续代码这样就会出现异常,回打印出10.
所以要保证上述代码的原子性。
1.原子操作:由一组相关的操作完成,这些操作可能会操纵与其它的线程共享的资源,为了保证得到正确的运算结果,
一个线程在执行原子操作其间,应该采取其他的措施使得其他的线程不能操纵共享资源。
2.解决办法:java为了解决这个问题,引进同步机制
具体的做法是在代表原子操作的程序代码前加上synchronized(Object)标记,这样的代码被称为同步代码块。
同类,代码块可以进行同步控制,这样把这个代码块放在一个方法中,就是同步方法了.
引入一个新的概念:同步锁:每个JAVA对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。
3.当一个线程试图访问带有synchronized(Object)标记的代码块时,必须获得 Object的对象的锁,
在以下的两种情况下,本线程有着不同的命运。
1、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。
锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,
并且转到就绪状态。
2、 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。
(一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁 如在执行同步代码块时,
遇到异常而导致线程终止,锁会被释放,还有就是在执行完代码后,也会释放锁;在执行代码块时,
执行了锁所属对象的wait()方法,这个线程会释放对象锁, 进入对象的等待池中)
4.线程同步的特征:
1、 如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的
同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一
个对象的同步代码块时,因为要获得对象的同步锁而互相牵制)
2、 每个对象都有唯一的同步锁
3、 在静态方法前面可以使用synchronized修饰符。
4、 当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行
Thread.sleep()或者执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。
5、 Synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,
除非用synchronized修饰
5.释放对象的锁:
1、 执行完同步代码块就会释放对象的锁
2、 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放
3、 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池。
6.死锁
什么叫死锁?为什么会发生死锁?
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。
JVM不监测也不试图避免这种情 况,因此保证不发生死锁就成了程序员的责任。
如何避免死锁
一个通用的经验法则是:当几个线程都要访问共享资源A、B、C 时,保证每个线程都按照同样的顺序去访问他们。
7.线程通信
Java.lang.Object类中提供了两个用于线程通信的方法
1、 wait():执行了该方法的线程释放对象的锁,JVM会把该线程放到对象的等待池中。该线程等待其它线程唤醒
2、 notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,
把它转到对象的锁池中。