1. 什么是线程
- 进程与线程区别:每个进程拥有自己独立的一套变量,而线程则共享数据;一个进程一般由多个线程组成;
- 线程创建方法:实现Runnable接口;继承Thread类;使用Callable和Future;使用线程池
//方法1:实现Runable接口
//Thread1.java
public class Thread1 implements Runnable{
public void run(){
System.out.println("dog");
}
}
//方法2:继承Thread
//Thread2.java
public class Thread2 extends Thread{
public void run(){
System.out.println("cat");
}
}
//方法3:实现Callable接口,并使用FutureTask进行包装
import java.util.concurrent.Callable;
public class Thread3 implements Callable<String> {//创建Callable接口的实现类
public String call(){
String str = "bird";
System.out.println("bird");
return str;
}
}
//Test.java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args){
System.out.println("hello");
Thread1 t1 = new Thread1();
Thread thr = new Thread(t1);
thr.start();
//new Thread(t1).start();
Thread2 t2 = new Thread2();
t2.start();
//new Thread2().start();
Callable<String> cal = new Thread3();
FutureTask<String> fut = new FutureTask<>(cal);//使用FutureTask类包装Callable对象
Thread thr3 = new Thread(fut);//实际上FutureTask实现了Runnable接口
thr3.start();
try {
//调用get()方法获得子线程执行结束后返回值
System.out.println(fut.get());//要求必须抛出异常
}catch (Exception e){
e.printStackTrace();
}
//使用Lambda表达式
FutureTask<Integer> fut2 = new FutureTask<Integer>(
(Callable<Integer>)()->{
System.out.println("dragon");
return 123;}
);
new Thread(fut2).start();
try{
System.out.println(fut2.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
- 中断线程:没有可以强制线程终止的方法;
interrupt
方法可用于请求终止线程;当调用interrupt
时,线程中断状态将被置位;若线程被阻塞,则无法检测中断状态;当对被阻塞线程(sleep wait)使用interrupt
方法时,阻塞调用将被Interrupted Exception
异常中断
//常用run()异常处理方法
public class Test {
public static void main(String[] args) {
Runnable r = () -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("exit");
}
};
Thread t = new Thread(r);
t.start();
try{
Thread.sleep(5000);}
catch (Exception e){
e.printStackTrace();
}
t.interrupt();//触发InterruptedException异常,执行finnally
}
}
- 线程重启:当线程退出后,不可再次使用start方法启用线程,正确做法应是再
new
一个线程,然后使用start
重新启动
2. 线程状态
线程可以有6种状态,可使用getState
获取线程当前状态
-
New(新建):new Thread®
-
Runnable(可运行): r.start();一个可运行线程可能正在运行也可能没有运行,取决于操作系统线程调度;
-
Blocked(被阻塞):当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态;当所有其他线程释放该锁,且调度器允许本线程持有它时,该线程变为非阻塞状态;
-
Waiting(等待):当线程处于被阻塞或等待时,它不运行任何代码,消耗资源最少;当线程等待另一线程通知调度器一个条件时,它自己进入等待状态;调用
Object.wait
或Thread.join
方法进入该状态 -
Timed waiting(计时等待):
Thread.sleep
、Object.wait
、Thread.join
、Lock.tryLock
、Condition.await
计时版 -
Terminated(被终止):两个原因,一是run方法正常退出而自然死亡,二是一个没有捕获的异常终止了run方法而意外死亡;虽可调用
stop
方法杀死线程,该方法抛出ThreadDeath
错误对象,由此杀死线程,但最好禁用;
3. 线程属性
-
线程优先级:
setPriority
设置,MIN_PRIORITY(0)
~MAX_PRIORITY(10)
,NORM_PRIORITY(5)
-
守护线程:
t.setDaemon(true)
将线程转换为守护线程,其唯一用途是为其他线程提供服务;守护线程应永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断; -
未捕获异常处理器:run方法不能抛出任何受查异常,但非受查异常会导致线程终止,在线程死亡之前,异常被传递到一个用于未捕获异常的处理器,该处理器必须属于一个实现
Thread.UncaughtExceptionHandler
接口的类,该接口仅有一个void uncaughtException(Thread t,Throwable e)
方法;可使用
setUncaughtExceptionHandler
方法为任何线程安装一个处理器,或使用Thread类静态方法setDefaultUncaughtExceptionHandler
为所有线程安装一个默认处理器;
4. 同步
- 锁对象:
ReentrantLock
类以及synchronized
关键字(synchronized将自动提供一个锁以及相关条件);锁是可重入的,因为线程可以重复地获取已经持有的锁;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
ReentrantLock mylock = new ReentrantLock();
mylock.lock();
try{
System.out.println("lock");
}
finally {
mylock.unlock();
}
}
}
- 条件对象:
newCondition
获取条件对象;当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await
、signalAll
或signal
方法
public void transfer(int from,int to,int amount){
bankLock.lock();
try{
while(accounts[from] < amount)
sufficientFunds.await();//Condition sufficientFunds;
...
sufficientFunds.signalAll();//通知正在等待的线程,此时可能已经满足条件
}
finally{
bankLock.unlock();
}
}
//void signalAll()解除该条件的等待集中的所有线程的阻塞状态
//void signal()从该条件的等待集中随机选取一个线程,解除其阻塞状态
synchronized
:若方法用synchronized
声明,则对象的锁将保护整个方法;内部对象锁intrinsicLock
仅有一个相关条件,wait
方法添加一个线程到等待集中,notifyAll
/notify
方法解除等待线程的阻塞状态,即wait
或notifyAll
等价于intrinsicCondition.await();
/intrinsicCondition.signalAll();
public synchronized void method(){
method body
}
//等价于
public void method(){
this.intrinsicLock.lock();
try{
method body
}
finally{
this.intrinsicLock.unlock();
}
}
静态方法声明为
synchronized
也是合法的,调用该方法将获得相关类对象的内部锁;内部锁和条件存在一定局限,如:
- 不能中断一个正在试图获得锁的线程
- 试图获得锁时不能设定超时
- 每个锁仅有单一的条件
- 同步阻塞:每个Java对象都有一个锁,线程可以通过调用同步方法获得锁,或通过进入一个同步阻塞而获得锁
synchronized (obj){
...
}//获取obj锁
- 监视器:监视器是仅包含私有域的类;每个监视器类的对象有一个相关的锁;使用该锁对所有方法进行加锁;该锁可以有任意多个相关条件
- Volatile域:仅仅为了读写一个或两个实例域就使用同步,显得开销过大;多处理器计算机能暂时在寄存器或本地内存缓冲区中保存内存中的值,结果导致了,运行在不同处理器上的线程可能在同一内存位置取到不同的值;编译器可以改变指令执行的顺序以使吞吐量最大化,这种顺序上的变化不会改变代码语义,但编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变,然而,内存的值可以被另一线程改变;若使用锁保护,编译器将被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令;
volatile
为实例域的同步访问提供了一种免锁机制,若声明一个域为volatile
那么编译器和虚拟机就知道该域是可能被另一线程并发更新的;
volatile变量不能提供原子性,如
public void f(){done=!done}
不能确保翻转域中的值,不能保证读取、翻转和写入不被中断
- final:声明域为
final
,多线程可安全地访问共享域(读写时仍需同步) - 原子性:若对共享变量除赋值外,并不完成其他操作,则可以将这些共享变量声明为
volatile
;类AtomicInteger
等提供了很多原子操作方法;若大量线程要访问相同的原子值,性能会大幅下降; - 死锁:Java还没有任何东西可以避免死锁现象,需要设计者仔细设计
- 线程局部变量:使用
ThreadLocal
辅助类为各个线程提供各自的实例,以避免使用共享变量; - 锁测试与超时:
lock
方法可能在申请锁时发生阻塞,tryLock
方法申请锁时,成功则返回true,否则立即返回false;tryLock
也可增加时间选项; - 读/写锁:
java.util.concurrent.locks
中ReentrantLock
和ReetrantReadWriteLock
类;ReetrantReadWriteLock
适合应用于多个线程从一个数据结构中读取数据而很少线程修改数据;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
public double getTotalBalance(){
readLock.lock();
try{...}
finally{readLock.unlock();}
}
public void transfer(...){
writeLock.lock();
try{...}
finally{writeLock.unlock();}
}