目录
一、线程的基本概念
1、线程的定义
线程被称为轻量级进程。像进程一样,线程在程序中是独立的、并发的执行路径,每个路径都有它自己的内存区域。但是,与分隔的进程相比,线程之间的隔离程度小,它们共享内存、文件句柄和其他每个进程应有的状态。一个进程可以支持多个线程,它们看似同时执行,但相互之间并不同步。
2、线程的优先级
Java定义了10个优先级,最高是10,最低是1。如果优先级超过1~10的范围,JDK则会抛出异常IllegalArgumentException。
预定义的优先级常量:MIN_PRIORITY = 1,MAX_PRIORITY = 10,NORM_PRIORITY = 5。
线程优先级的特性:
继承性:线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级和A线程相同。
规则性:CPU尽量将资源让给优先级比较高的线程,即线程的优先级越高,能抢占到CPU资源的机会越大。
随机性:优先级高的线程并不一定每次都能抢到CPU资源。线程的优先级具有“随机性”,优先级高只是代表抢到线程资源的概率更大。
3、守护线程
Java中的线程分为两种,用户线程和守护线程(Daemon)。守护线程是用来为其他线程的运行提供便利服务的。只有在存在用户线程的情况下,才会存在守护线程;如果用户线程执行结束后,一段时间后()守护线程也会自动销毁。最典型的守护线程,就是GC垃圾收集器。需要注意的是,用户也可以通过setDaemon(true)方法自定义线程为守护线程。
二、线程的创建方式
线程的创建有两种方式。
1、继承Thread类
1、实现步骤:
1)定义一个类继承Thread类,重写其中的run()方法,将需要执行的操作放入到run()方法中;
2)创建该Thread子类的对象;
3)调用该对象的start()方法,启动线程。
2、代码示例:
子线程代码:
class MyThread extends Thread{
public void run() {
System.out.println("线程运行了");
}
}
主函数:
public static void main(String[] args) {
MyThread thread = new MyThread();
thread. start ();
}
3、注意事项:
1)要想启动线程,必须调用线程的start()方法;不能直接运行run()方法,这样只是在主线程中调用了该方法,并没有 创建新的线程;
2)这种方式有一个缺点,由于Java的单继承性,继承Thread的类不能再继承其他的父类,这在一些情况下是很不方便的。
2、实现Runnable接口
1、实现步骤:
1)生成一个Runnable接口实现类的对象;
2)将Runnable实现类的对象传入到Thread的构造器中,创建Thread的对象;
3)Thread的对象执行start()方法,启动线程。
2、代码示例:
子线程代码:
class RunnableImpl implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("线程运行起来了");
}
}
主线程的代码:
public static void main(String[] args) {
RunnableImpl impl = new RunnableImpl();
Thread thread = new Thread(impl);
thread.start();
}
3、注意事项:
1)相对于前一种方式,这种创建线程的方式更推荐使用;
2)实际上,Thread类也是实现了Runnable接口。
三、线程的主要方法
1、线程状态的转换
线程的方法,主要都是围绕着线程状态的转换。线程状态转换示意图如下:
1、sleep():
静态方法,在指定的毫秒数内让当前正在执行的线程休眠。参数num指的是休眠的毫秒数。
线程休眠期间,暂停执行,放弃CPU资源,但是占据的同步锁不会释放。
2、interrupt():
在当前线程中打上停止的标志,但是不会马上停止线程。一般和interrupted()方法或者isInterrupted()搭配使用。
3、yield():
静态方法,使当前正在执行的线程放弃CPU资源,并重新和其他线程争抢CPU资源。
4、join():
调用某线程的该方法,将当前线程与该线程“合并”,即等待该线程结束,再恢复当前线程的运行。
5、wait()和notify()/notifyAll():
wait():当前线程进入对象的wait pool,等待被notify或者notifyAll的通知唤醒。
notify/notifyAll:唤醒对象的wait pool中的一个/所有等待线程
6、suspend()和resume():
suspend():在操作系统的层面挂起当前线程。
resume():接触当前线程的挂起状态。
suspend方法导致线程被挂起后,并不会释放持有的锁,如果迟迟不给挂起的线程执行resume方法,会导致线程出现死锁的情况,所以这对方法不提倡使用。
7、stop():
终止当前线程的执行。stop会暴力停止线程,导致数据的完整性被破坏,不提倡使用。例如任务需要将{x=3, y=3}的x和y分别+1,如果任务执行完x+=1后执行stop方法,这时候就会出现{x=4, y=3}的情况,数据的完整性就被破坏了。
2、线程自身信息的获取和设置
1、currentThread():
静态方法,返回当前正在执行的线程,返回值是Thread或者Thread子类的实例。
2、getId():
取得线程的唯一标识。
3、setName(String name)和getName():
设置/获取 线程的名称。
4、isAlive():
判断当前线程是否处于活动状态,即是否处于正在运行或准备开始运行的状态。返回布尔值,处于活动状态返回true。
5、setPriority()和getPriority():
设置/获取 线程优先级。
6、interrupted():
静态方法,测试当前线程是否已经是中断状态,返回布尔值。执行后会将状态标志置为false。
7、isInterrupted():
测试线程对象Thread是否已经是中断状态,但不清楚状态标志。返回布尔值。
8、setDaemon(boolean on):
setDaemon(true)表示设置线程为守护线程,setDaemon(false)表示设置线程为用户线程。
四、线程状态的转换
1、线程的状态
Runnable:指的是可运行、或者说“就绪”状态。在这种状态下,线程等待抢夺CPU资源,如果抢到CPU资源,就可以直接执行。
Running:指的是抢到了CPU资源、正在运行的状态。
Blocked:阻塞状态,这种状态的线程不参与CPU资源的竞争。
2、线程进入Blocked状态:
1)线程调用了sleep方法,主动放弃占用的处理器资源;
2)线程调用了阻塞式IO方法,在该方法返回前该线程被阻塞;
3)线程试图获得一个同步监视器,但该同步监视器正被其他线程持有。
4)线程处于wait状态,正在等待某个通知;
5)程序调用了suspend方法将线程挂起(此方法容易导致死锁)。
3、线程进入Runnable状态:
1)线程调用sleep()方法足够时间后休眠结束;
2)线程调用的阻塞IO已经返回,阻塞方法执行完毕;
3)线程成功地获得了正在等待的同步锁;
4)线程正在等待某个通知,其他线程发出了通知;
5)处于挂起状态的线程调用了resume恢复方法。
3、线程进入Running状态的情况:
当且仅当Runnable状态的线程占用CPU资源之后。
4、结束线程的4种方式
停止线程,就是在线程处理完任务之前停止正在做的操作,结束线程的执行。
1、自然结束
线程的执行,实质上就是通过start()方法启用新线程来执行run()的过程。那么当run()方法执行到末尾的时候,方法就会自然结束。推荐使用这种方式。
2、使用interrupt()停止线程
interrupt()方法和interrupted()/isInterrupted()方法结合使用,可以实现让线程停止。
通过使用interrupt()方法设置线程的中断标志为true,然后在线程中通过interrupted()/isInterrupted()方法检测线程的中断标志的值,如果为true则表示需要线程中断,则在线程的run()方法中使用return关键字中断线程的执行。
示例:
if (this.isInterrupted()) {
return;
}
这种方法在需要在线程任务执行期间结束线程的情况下,可以使用。
3、抛出异常
线程捕获异常后,立即转去执行catch和finally中的内容,执行完catch和finally中的内容后直接退出线程的执行过程,不再执行线程的其他内容。
可实现的方式有:
1)对sleep()状态中的线程执行interrupt():会导致该线程抛出InterruptedException从而进入catch语句,并将停止状态值置false。
2)对已执行interrupt()的线程执行sleep():也会导致线程进入catch语句,停止状态值在执行interrupt()方法的时候已经置为false。
4、stop()方法暴力停止
使用stop()方法可以强制停止线程,这种方式不推荐使用,因为可能导致一些清理工作得不到完成,也可能导致加锁对象产生数据一致性方面的问题。
调用stop()方法时会抛出java.lang.ThreadDeath异常,但是通常这个异常不需要显式捕捉。
stop()方法会释放锁。通过stop()方法释放锁会导致共享数据不能及时从线程的工作内存中同步到主内存,产生数据不一致的问题。
五、线程同步和锁的基础
1、临界资源和互斥锁
临界资源:多个线程间共享的数据称为共享资源或临界资源。
只有对象的成员变量或者类变量(被static修饰的变量),才可能是临界资源,因为这些变量被分配在线程共享的堆中;方法中定义的变量不会是临界资源,不需要考虑线程同步的问题,因为方法中定义的变量都分配在线程私有的栈中。所以,方法体内部定义的变量一定是线程安全的。
临界资源问题:由于线程调度器负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
互斥锁(同步锁):Java中,为保证共享数据操作的完整性,引入了对象互斥锁的概念,每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能由一个线程访问该对象。
2、死锁和死锁的解决
线程的死锁:当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。而由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。
死锁解决:避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序;并按照与获取相反的次序释放锁。通过画资源分析图,从根源上避免死锁。
3、释放锁的时机
由于等待对象锁标志的线程在得到锁标志之前不能恢复运行,所以让持有锁标志的线程在不再需要锁的时候返回锁标志是很重要的。
锁标志释放的时机:
1)持有锁标志的线程执行到synchronized()代码块末尾时将释放锁;
2)Java中出现中断或异常也会使执行流跳出synchronized()代码,锁自动返回;
3)线程持有特定锁,该对象锁可以通过调用wait()方法,使当前持有该锁线程进入阻塞状态并释放持有的锁。这里需要注意的是,调用wait()方法的并不是线程,而是对象锁!
注意:这里再强调一遍,sleep()方法会使当前线程释放CPU,但是并不会释放锁!!!
4、synchronized关键字
Java的对象,默认是可以被多线程共用的,在需要时才启动“互斥”机制,称为专用对象。当某个对象用synchronized修饰时,表明该对象已启动“互斥”机制,在任一时刻只能由一个线程访问。即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象。
synchronized的使用方法:
1)同步代码块:形如synchronized(object){},其中()中的内容,表示加锁对象;{}中的是被加锁的代码块。
2)同步方法:用在方法声明中,表示整个方法为同步方法。同步方法的加锁对象类似于synchronized(this),this表示当前对象。
注意事项:
1)添加同一个锁的代码块或者方法,同时只能有一个线程执行。例如某个对象有get()方法和set()方法,都是synchronized方法,那么当有一个线程在执行get()方法的时候,其他线程也是不能执行set()方法的。
2)当synchronized用在静态方法中的时候,表示加锁的目标是整个类,这个类的所有对象都要遵循synchronized的同步规则。
5、可见性和volatile关键字
1、可见性:
当一个线程修改了变量,其他线程可以立即知道。由于线程是在线程的私有的栈中执行方法的,所以对于共享资源,如果线程修改了共享共享对象,其他对象并不会马上知道,所以可见性的实现需要一些额外的操作。/
2、保证可见性的方法:
1)volatile关键字:
2)synchronized :unlock之前,写变量值回主存
3)final:一旦初始化完成,其他线程就可见
3、volatile关键字:
volatile关键字的主要作用是使变量在多个线程见可见。
volatile会强制线程每次使用volatile变量前从公共堆栈中取得变量的值、每次修改完变量值更新到公共堆栈中,而不是从线程私有数据栈中读写变量。
4、volatile和synchronized对比:
1)volatile是线程同步的轻量级实现,性能要高于synchronized;
2)volatile只能修改变量,而synchronized可以修饰方法以及代码块;
3)多线程访问volatile不会发生阻塞,但是synchronized会;
4)volatile能保证数据的可见性,但是不能保证数据的原子性;synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步;
5)volatile是用于解决变量在多线程间的可见性,而synchronized是用于解决多线程之间资源访问的同步性。
5、volatile的非原子性:
会导致volatile不能保证变量读写的线程安全,例如前一个线程执行了写操作但是还没有来得及更新到主内存,这时候下一个线程读到的还是前一个线程修改之前的变量值。
6、有序性和指令重排
有序性:
在本线程内,操作都是有序的;在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
指令重排:
一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,更改指令的执行顺序。在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
不可重排语句:
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
可重排语句:a=1;b=2;
这样,在多线程的情况下,如果其他线程会访问到a和b,就可能会出现逻辑问题,比如可能先执行a=1再执行b=2。
添加synchronized关键字可以避免指令重排,因为可以让两个线程串行。
指令重排的基本原则
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C 那么A必然先于C
线程的start方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
六、线程间通信
1、等待/通知机制
1、while/sleep机制
通过while(true)死循环和sleep()方法的组合使用,可以实现在线程间通信。
线程A:
for(int i=0;i<10;i++) {
list.add(i);
System.out.println("list添加了元素:"+i);
Thread.sleep(100);
}
线程B:
while(true) {
if (list.size() == 5) {
System.out.println("list.size==5,线程B退出!");
throw new InterruptedException();
}
}
这种方式是非常非常低效的。线程B不停的通过while语句轮询机制来查询一个条件,这种方式非常浪费CPU资源。如果轮询的时间间隔很小,更加浪费CPU资源;但是如果轮询的时间间隔很大,就有可能得不到想要的数据。
2、wait/notify机制
1、wait()方法:
方法介绍:Object中定义的方法,用来使执行当前加锁代码的线程进行阻塞状态进行等待。调用该方法后,持有该锁的线程将被放入“预执行队列”,并且阻塞在wait()所在的代码处,直到接到通知或者被中断为止。
使用前提:在执行wait()方法前,线程必须获得该对象的对象锁,即只能在同步方法或同步代码块中调用wait()方法。
使用效果:在执行wait()方法后,当前线程释放锁,并阻塞在wait()方法处。
注意事项:在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()的代码不在同步方法或者同步代码块中,则会抛出IllegalMonitorStateException异常。这是RuntimeException的一个子类,不需要try-catch捕获。
2、wait(long timeout)方法:
带参数的wait(long timeout)方法。和wait()方法的唯一区别,是wait(long timeout)会在等待超时后自动唤醒。超时的时间是通过输入参数进行设置的,单位是毫秒。
3、notify()方法:
方法介绍:Object中定义的方法,用来通知可能等待该对象的对象锁的其他线程。如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。
使用效果:执行notify()方法后,当前持锁线程不会立刻释放该对象锁,而等到执行完成同步方法或者同步代码块之后,才会释放锁,呈wait状态的线程才可以获得对象锁。
注意事项:notify()方法也要在同步方法或同步块中调用,否则也会抛出IllegalMonitorStateException。
4、notifyAll()方法:
方法介绍:可以使所有因为执行该对象的wait()方法而进入等待队列的线程,从等待状态中退出,进入可运行状态。此时,哪个线程最先执行,取决于JVM的实现。
notifyAll方法和notify方法的区别只在于,notify方法只会随机唤醒一个线程,而notifyAll方法会唤醒全部等待线程。
5、特殊情况:
interrupt()遇到wait():当线程处于wait等待状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。
6、示例:
示例可以参考下一节的生产者和消费者模型。
3、生产者/消费者模型
1、单生产/单消费:操作值
本例中,生产者产生值,消费者清空值。
生产者的方法体:
synchronized (lock) {
if (!ValueObject.value.equals("")) {
lock.wait();
}
ValueObject.value = System.currentTimeMillis()+"";
lock.notify();
}
消费者的方法体:
synchronized (lock) {
if (ValueObject.value.equals("")) {
lock.wait();
}
ValueObject.value = "";
lock.notify();
}
存储值的对象:
class ValueObject{
public static String value="";
}
备注:对于上述模型,如果增加生产者和消费者,变成多生成多消费的模型,则可能会导致生产者唤醒生产者、或者消费者唤醒消费者的情况,这样唤醒同类的情况一直累计,就会产生“假死”。
2、多生产与多消费:操作值
需要将上述模型中的notify()方法修改为notifyAll()方法,这样可以唤醒全部的类,包括其他全部的生产者和消费者。这样可以有效避免生产者唤醒另一个生产者,或者消费者唤醒另一个消费者,导致程序无法执行出现“假死”的情况。
3、一生产与一消费:操作栈
本例中生产者向栈List对象中放入数据,消费者从List栈中取出数据。List的最大容量是1。
生产者的方法体:
synchronized (lock) {
if (!ValueObject.value.equals("")) {
lock.wait();
}
myStack.push();
lock.notify();
}
消费者的方法体:
synchronized (lock) {
if (ValueObject.value.equals("")) {
lock.wait();
}
myStack.pop();
lock.notify();
}
自定义栈:
class MyStack{
private List<String> list = new ArrayList<String>();
synchronized public void push() throws InterruptedException {
if (list.size() == 1) {
this.wait();
}
list.add("anyString="+Math.random());
this.notify();
}
synchronized public String pop() throws InterruptedException {
if (list.size() == 0) {
this.wait();
}
String returnValue = list.get(0);
list.remove(0);
this.notify();
return returnValue;
}
}
4、多生产与多消费:操作栈
这时包含三种具体情况:一生产与多消费、多生产与一消费、多生产与多消费。这里可能遇到的问题以及解决方案,以一生产与多消费这种情况为例。
问题点1:wait条件改变导致的抛出异常。
问题描述:因为有多个消费者,所以肯定会存在连续两个或两个以上的消费者连续被唤醒的情况。如果一个被唤醒的消费者,它前一个被唤醒的线程也是消费者,这时的list中内容是空的;这种情况下,如果这个消费者不是第一次执行,那么它会继续上次执行到的地方继续执行。按上一模块的代码,这个消费者执行的是if判断之后的内容,即此时会执行list.remove(0);但是list已经在上一个消费者被唤醒时执行过remove操作了,这个消费者对一个空的list执行remove,会抛出异常。
解决方案:将if语句修改为while语句,这样消费者再次执行的时候,就会继续执行while语句,判断是否满足wait的条件。
问题点2:连续唤醒同类导致的假死问题。
解决:将notify()方法换成notifyAll()方法。这个类似于多生产者/多消费者:操作值的情况。
2、等待另一个线程的执行结果:join()方法
应用场景:主线程创建并启动子线程后,如果子线程要进行耗时操作,而主线程需要等待子线程执行完成后再结束,这时就要使用join方法。join方法的作用就是等待线程对象销毁。
应用方式:在thread1中执行:thread2.join(),那么thread1将进行无限期阻塞,等待thread2销毁后再执行thread1后面的内容。
join期间被中断:在thread1中执行:thread2.join()后,如果在thread3中执行thread2.interrupt()方法,thread2将抛出异常并停止执行,但thread1仍会继续执行。也就是说,处于join等待状态的线程被执行interrupt()方法后会抛出异常,但是不会影响到被等待的线程的状态。
join(long timeout):long型参数timeout是用来设定等待时间的,单位是毫秒。表示等待线程最多只等待参数time指示的时间长度,如果超时以后被等待线程还在运行,等待线程将不会再等待,而是继续执行后面的代码。
join(long timeout)和sleep (long timeout)的区别:
1)join(long time)的功能是在内部使用wait(long time)来实现的,所以join(long time)具有释放锁的特点,而sleep(long time)不释放锁;
2)join(long time)是非静态方法,而sleep(long time)是静态方法。
3、InheritableThreadLocal类
1、ThreadLocal
类ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子里可以存储每个线程的私有数据。
ThreadLocal解决的是变量在不同线程间的隔离性,不同线程的值可以放入ThreadLocal类的实例中分别保存。
1、get()方法与set(String s)方法:
threadLocal.get():返回threadLocal中存放的内容。如果threadLocal中没有内容,将返回null。
threadLocal.set(String s):向threadLocal中存放内容
2、复写initialValue()方法:
通过继承ThreadLocal并复写initialValue()方法,可以为ThreadLocal子类的实例设置初始值,这样在调用set方法为ThreadLocal实例赋值前,ThreadLocal的get方法返回的就是initialValue()方法return的值。
protected Object initialValue(){
return "initial value";
}
2、InheritableThreadLocal
使用InheritableThreadLocal类可以让子线程从父线程中取得值。
1、复写initialValue()方法改变初始值:继承InheritableThreadLocal并复写initialValue()方法,可以为InheritableThreadLocal子类的实例设置初始值。
2、在子类中调用get()方法,会取得初始值或父线程中设置的值;在子线程中调用set()方法,设置的值在父线程中调用get()也会访问到。
一般是定义一个包装类,在里面定义InheritableThreadLocal子类的实例,然后分别再从父线程和子线程中进行访问。
4、线程间数据传输:管道流
JDK提供了两组管道流,用于在不同线程间直接传送数据:
1)PipedInputStream 和 PipedOutputStream;
2)PipedReader 和 PipedWriter。
1、管道字节流
使用步骤:
1)通过构造器生成管道流的实例:
PipedInputStream input = new PipedInputStream();
PipedOutputStream output = new PipedOutputStream();
2)建立输入流和输出流的连接:
input.connect(output);或者output.connect(input);
3)分别把管道流的实例传递给不同线程,调用方法进行通信:
output.write(byte b):把byte类型数据流写入管道;
input.read(byte [] byteArray):从管道中读取数据并写入到byte数组,方法返回值为读取到的内容的长度。
4)关闭流:.close()方法。
2、管道字符流
使用步骤:
1)通过构造器生成管道流的实例:
PipedReader reader = new PipedReader();
PipedWriter writer = new PipedWriter();
2)建立输入流和输出流的连接:
reader.connect(writer);或者writer.connect(reader);
3)分别把管道流的实例传递给不同线程,调用方法进行通信:
output.write(String s):把byte类型数据流写入管道;
input.read(char [] charArray):从管道中读取数据并写入到byte数组,方法返回值为读取到的内容的长度。
4)关闭流:.close()方法。