Chap1. Java多线程技能
进程是受操作系统管理的基本运行单元 线程是在进程中独立运行的子任务
使用多线程的方法
- 继承
Thread
类:继承自Runnable
接口 - 继承
Runnable
接口:可以支持多继承
Thread.java
类中的start()
方法是异步执行,此线程对象交给“线程规划器”处理,而run()
方法是同步执行,由main()
主线程来调用run()
方法。
线程安全: synchronized
可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
留意i--
与System.out.println()
的异常: 如果在虽然println
函数中i--
,如
System.out.println("i=" + (i--));
println
方法在内部是同步的,但i–的操作却是在进入println()
之前发生的,所以有发生非线程安全问题的概率。
基本API:
currentThread()
方法:返回代码段正在被哪个线程调用。isAlive()
方法:判断当前线程是否处于活动状态。sleep()
方法:在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行),这个“正在执行的线程”是指this.currentThread
返回的线程。getID()
方法:作用是取得线程的唯一标识。yield()
方法:放弃当前CPU资源,将它让给其他的任务去占用CPU执行时间。
停止线程: stop()
方法已弃用,可能导致数据不同步。 一般使用interrupt()
方法,相当于在当前线程中打了一个停止标记,使程序进入中断状态,但该方法不能让线程立即停止,若要使线程立即停止,需要加上异常处理。
判断线程是否已经中断:
interrupted()
方法:调用该方法后会清除线程的中断状态。isInterrupted
方法:作用与前者相同,但不会清除中断状态。
暂停线程 用suspend()
方法暂停线程,用 resume()
方法恢复线程。缺点是容易导致对公共的同步对象的独占,使其他线程无法访问公共同步对象,且容易导致数据不同步,目前已弃用。
线程的优先级 优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
- 继承特性:A线程启动B线程,则B线程的优先级与A是一样的。
- 规则性:高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。
- 随机性:优先级较高的线程不一定每次都先执行完。
守护线程 Java中有两种线程,用户线程和守护线程。 守护线程的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型饿守护线程就是垃圾回收线程。
Chap2. 对象及变量的并发访问
synchronized关键字
synchronized同步方法
“非线程安全”会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据是被更改过的。 “线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
在实现方法内部声明一个变量时,是不存在“非线程安全”问题的,如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题,只需要在访问实例变量的方法前加上synchronized
关键字,即可解决问题。
关键字synchronized
取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized
关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
调用关键字synchronized
声明的方法一定是排队运行的,且只有共享资源的读写访问才需要同步化。但假如线程A持有某对象的锁,那线程B异步调用非synchronized类型的方法不受限制。
锁重入:关键字synchronized
拥有锁重入的功能,也就是在使用synchronized
时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized
方法/块的内部调用本类的其他synchronized
方法/块时,是永远可以得到锁的。如果不可锁重入的话,就会造成死锁。 当存在父子类继承关系时,子类可以通过“可重入锁”调用父类的同步方法。
此外,当一个线程执行的代码出现异常时,其所持有的锁会自动释放。且同步不能被继承,如果子类想实现同步,还得在子类的方法中添加synchronized
关键字。
synchronized同步语句块
使用synchronized
声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待较长时间,在这样的情况下可以使用synchronized
同步语句块来解决。不在synchronized
块中的代码就是异步执行,在synchronized
中的代码就是同步执行。
在使用同步synchronized(this)
代码块时需要注意的是,当一个线程访问object的一个synchronized(this)
同步代码块时,其他线程对同一个object中所有其他synchronized(this)
同步代码块的访问将被阻塞,这说明synchronized
使用的对象监视器是一个。
将任意对象作为对象监视器 synchronized(非this对象x)
同步代码块:
- 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行
synchronized(非this对象x)
同步代码块中的代码。 - 在持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行
synchronized(非this对象x)
同步代码块中的代码。
锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this对象x)
代码块中的程序与同步代码块是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。此外,使用synchronized(非this对象x)
同步代码块也可以解决“脏读”问题。
synchronized(非this对象x)
格式的写法是将x对象本身作为“对象监视器”,这样就可以得出下面三个结论:
- 当多个线程同时执行
synchronized(x){}
同步代码块时呈同步效果 - 当其他线程执行x对象中
synchronized
同步方法时呈同步效果 - 当其他线程执行x对象方法里的
synchronized(this)
代码块时呈同步效果
静态同步synchronized
方法与synchronized(class)
代码块 关键字synchronized
还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁。本质上说,synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。要注意Class锁可以对类的所有对象起作用。而同步synchronized(class)代码块
与synchronized static方法的作用一样。
数据类型String的常量池特性: 在JVM中具有String常量池的功能,将synchronized(string)
同步块与String联合使用时可能会产生一些意外,因此大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如new Object()
实例化一个Object对象,但它并不放入缓存中。
多线程的死锁 死锁是程序设计的bug,在设计程序时就要避免双方互相持有对方的锁的情况。
锁对象的改变:线程A和B持有的锁都是“123”,如果将锁改成“456”,结果还是同步的,因为A和B共同争抢的锁是“123”。只要对象不变,即使对象的属性被改变,运行的结果还是同步。
volatile关键字
主要作用是使变量在多个线程间可见。volatile
可以强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。使用volatile
关键字可以解决私有堆栈中的值和公共堆栈中的值不同步的问题。 但是volatile
关键字有一个最致命的缺点:不支持原子性。
synchronized
和volatile
比较:
- 关键字volatile是线程同步的轻量级实现,性能比synchronized好,且volatile只能修饰变量,synchronized可修饰方法和代码块。
- 多线程访问volatile不会发生阻塞,synchronized会出现阻塞。
- volatile能保证数据可见性,不保证原子性;synchronized可以保证原子性,也可以间接保证可见性,因为synchronized会将私有内存和公共内存中的数据做同步。
- volatile解决的是变量在多个线程间的可见性,synchronized解决的是多个线程访问资源的同步性。
Chap3. 线程间通信
等待/通知机制
wait()
方法:使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待序列,直到被再次唤醒。notify()
方法:随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待序列,进入可运行状态,也就是notify()
方法仅通知一个线程。notifyAll()
方法:使所有正在等待队列中等待同一共享资源的全部线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能随机执行,这要取决于JVM的实现。
每个锁对象有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度;反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。
当方法wait()
执行后,锁被自动释放,但执行完notify()
方法,锁却不自动释放。 当线程呈wait()
状态时,调用线程对象的interrupt()
方法会出现InterruptedException异常。 带一个参数的wait(long)
方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。
生产者/消费者模式 两种错误的避免: 1、如果采用wait/notify
进行通信,程序运行中很有可能出现假死状态,呈假死状态的进程中所有的线程都呈WAITING状态。这是因为程序不能保证notify唤醒的是异类,比如“生产者”唤醒“生产者”或“消费者”唤醒“消费者”。如果按这样的情况运行的比率积少成多,就会导致所有线程都不能继续运行下去,均呈WAITING状态,导致程序“假死”。 解决方法是将notify()
改成notifyAll()
方法,它的原理是不光通知同类线程,也包括异类,这样可以使程序一直运行下去。 2、如果在程序中使用if
语句作为条件判断,当条件发生改变时并没有得到及时的响应,所以多个呈wait状态的线程被唤醒,继而出现异常,解决办法是把if
全部换成while
语句。
管道通信 管道流(pipeStream)可以用于在不同线程间直接发送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信,而无须借助于类似临时文件之类的东西。
- 字节流:PipedInputStream和PipedOutputStream
- 字符流:PipedReader和PipedWriter
join方法
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()
方法。方法join
的作用是等待线程对象销毁。
方法join()
的作用是使所属的线程对象x正常执行run()
方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。
方法join()
具有使线程排队的作用,有些类似同步的运行效果。join与synchronized的区别是:join()
在内部使用wait()
方法进行等待,而synchronized
关键字使用的是“对象监视器”原理作为同步。 方法join()
与interrupt()
如果彼此遇到,则会出现异常。 方法join(long)
可设定等待时间。join(long)
的功能内部使用wait(long)
方法实现,所以join(long)
方法具有解放锁的特点。而Thread.sleep(long)
方法则不释放锁。
ThreadLocal类
变量值的共享可以使用public static变量的形式,所有的线程都使用同一个public static变量。但如果想实现每一个线程都有自己的共享变量,则需要使用ThreadLocal
类。 类ThreadLocal
主要解决的就是每个线程绑定自己的值,可以将ThreadLocal
类比喻呈全局存放数据的盒子,盒子中可以存储每个线程的私有数据。
- 通过
get()
和set()
方法可以获取和设置每个线程的私有数据。 - 覆写该类的
initialValue()
方法可以使变量初始化,从而解决get()
返回null的问题。 InheritableThreadLocal
类可在子线程中取得父线程继承下来的值。
Chap4. Lock的使用
使用Java5中出现的Lock对象同样也能实现同步的效果,且还具有嗅探锁定、多路分支通知等功能。比使用synchronized
更加灵活。
ReentrantLock类
使用lock()
方法获取锁,unlock()
方法释放锁。 使用Condition
实现等待/通知:
- Object类中的
wait()
方法相当于Condition类中的await()
方法。 - Object类中的
wait(long timeout)
相当于Condition类中的await(long time, TimeUnit unit)
方法。 - Object类中的
notify()
方法相当于Condition类中的signal()
方法。 - Object类中的
notifyAll()
方法相当于Condition类中的signalAll()
方法。
在使用notify()
/notifyAll()
方法进行通知时,被通知的线程是由JVM随机选择的,但使用ReenTrantLock
结合Condition
类是可以实现“多路通知”的,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择地进行线程通知,在调度线程上更加灵活。
公平锁与非公平锁 公平锁表示线程获取锁地顺序是按照线程加锁的顺序来分配的,即FIFO顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,这个方式可能造成某些线程一直拿不到锁。
ReentrantReadWriteLock类
类ReentrantLock
具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()
方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率非常低下。所以JDK中提供了一种读写锁ReentrantReadWriteLock
类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock
来提升该方法的代码运行速度。
读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也称为排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
Chap5. 定时器Timer
定时/计划功能在移动开发领域应用较多,如Android技术。定时机会任务功能在Java中主要使用的就是Timer对象,它在内部是用多线程的方式进行处理的。
Timer
类主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务。封装任务的类则是抽象类TimerTask
,执行计划任务的代码要放入TimerTask
的子类中。
相关API
方法 | 说明 |
---|---|
int getHoldCount() |
查询当前线程保持此锁定的个数,即调用lock()方法的次数 |
int getQueueLength() |
返回正在等待获取此锁定的线程估计数 |
int getWaitQueueLength(Condition condition) |
返回等待与此锁定相关的给定条件Conditon的线程估计数 |
boolean hasQueueThread(Thread thread) |
查询指定的线程是否正在等待获取此锁定 |
boolean hasQueueThreads() |
查询是否有线程正在等待获取此锁定 |
boolean hasWaiters(Condition) |
查询是否有线程正在等待与此锁定有关的condition条件 |
boolean isFair() |
判断是不是公平锁 |
boolean isHeldByCurrentThread() |
查询当前线程是否保持此锁定 |
boolean isLocked() |
查询此锁定是否由任意线程保持 |
void lockInterruptibly() |
如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常 |
boolean tryLock() |
仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定 |
boolean tryLock(long timeout,TimeUnit unit) |
如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定 |
Chap6. 单例模式与多线程
立即加载/“饿汉模式”
使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。
延迟加载/“懒汉模式”
延迟加载就是在调用get()方法时才被创建,常见的实现办法是在get()方法中进行new实例化。 缺点:在多线程环境中,会出现取出多个实例的情况,与单例模式的初衷违背。 解决方案:
- 声明
synchronized
关键字:运行效率低下,下一个线程想要取得对象,必须等上一个线程释放锁后,才可以继续执行。 - 使用同步代码块:与第一种方法一样同步运行,效率低下
- 针对某些重要的代码进行单独的同步:只对实例化对象的关键代码进行同步,还是有线程安全问题
- 使用DCL双检查锁机制:对实例对象加上
volatile
关键字,在关键代码块上对加了volatile
关键字的实例变量的类加上synchronized
锁,可以成功解决“懒汉模式”遇到的多线程问题。 - 使用静态内置类实现单例模式:可以实现和DCL相同的效果。