从0开始复习Java(10)
一、线程概述
进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程的三个特征:
- 独立性
- 动态性
- 并发性
线程可以拥有自己的堆栈、程序计数器、局部变量,但不拥有系统资源,它与父进程的其它线程共享该进程所拥有的全部资源。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并发执行。
二、线程的创建和启动
Java使用Thread
类创建线程,所有的线程对象都必须是Thread
类或其子类的实例。
继承Thread创建线程
- 定义Thread类创建线程类,并重写
run()
方法,表示线程需要完成的任务,run()
方法为线程执行体。 - 创建Thread子类的实例,即创建了线程实例。
- 调用线程对象的start()方法启动该线程。
public class Test extends Thread {
private int i ;
@Override
public void run(){
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
new Test().start();
new Test().start();
}
}
以上程序有3个进程,包括主线程。main方法的方法体代表主线程的线程执行体。
- Thread.currentThread()
返回正在执行的线程对象
- getName()
返回调用该方法的线程的名字。
程序可以通过setName(String name)方法为线程设置名字,通过getName()方法返回名字。默认主线程的名字为
main
,用户启动的多个线程的名字依次为Thread-0
,Thread-1
...
使用继承Thread类的方法创建的线程类,多个线程之间无法共享线程类的实例变量。
实现Runnable接口
- 定义Runnable接口的实现类,并重写run()方法
- 创建Runnable实现类的实例,并以此实例作为Thread的target创建Thread对象。
- 调用run()方法启动线程
//实现Runnable接口的类的实例
Test t = new Test();
new Thread(t);
new Thread(t, "name");
Runnable对象仅仅作为Thread对象的target,Runnable实现类里面的run()方法仅作为线程执行体。实际的线程对象依然是Thread的实例,该Thread线程负责执行其target的run()方法。
Runnable接口是函数式接口,Callable接口也是函数式接口
使用Callable和Future
Java5开始提供Callable
接口,提供了一个call()
方法可以作为线程执行体,但比run()
方法更强大:
- call()方法可以有返回值
- 可以声明抛出异常
Callable对象不能直接作为Thread的target。
java5提供了Future接口代表Callable接口里call()方法的返回值。并为Future接口提供了FutureTask实现类。该实现类实现了Runnable接口,可以作为Thread的target。
Future接口定义的公共方法控制它关联的Callable任务:
- boolean cancel(boolean mayInterruptIfRunning)
- V get()
- V get(long timeout, TimeUnit unit)
- boolean isCancelled()
- boolean isDone()
Callable接口有泛型限制,泛型形参与call()方法的返回值类型相同。而且Callable接口是函数式接口。
创建并启动有返回值的接口:
- 创建Callable接口的实现类,并实现call()方法,再创建Callable实现类的实例。
- 使用FutureTask类包装Callable对象,封装了Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread的target创建并启动新线程。
- 调用FutureTask对象的get()方法获得子线程执行结束后的返回值
import java.util.concurrent.FutureTask;
public class Test{
public static void main(String[] args) {
Test test = new Test();
FutureTask<Integer> task = new FutureTask<>( ()->{
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
});
new Thread(task).start();
try{
System.out.println(task.get());
}
catch(Exception e){
e.printStackTrace();
}
}
}
创建线程的三种方式对比
可以将Callable
和Runnable
归并为一类。
使用实现这两个接口创建多线程的优缺点:
- 还可以继承其他类
- 多个线程可以共享同一个target对象
- 编程稍微复杂,如果需要访问当前线程,则要使用
Thread.currentThread()
方法
使用Thread
方式:
- 不能继承其他父类
- 编写简单
推荐使用
Runnable
和Callable
方式创建多线程
三、线程的生命周期
新建、就绪、运行、阻塞、死亡
新建和就绪状态
new之后就处于新建状态。分配内存,初始化成员变量的值。
start之后处于就绪状态。创建方法调用栈和程序计数器。
运行和阻塞状态
处于就绪状态的进程获得了cpu就处于运行状态。
所有现代的桌面和服务器操作系统都采用抢占式调度策略。(系统分配给线程一段时间)
协作式调度策略(线程调用它的sleep或者yield方法才会停止)
以下情况由就绪变为阻塞:
- 调用sleep方法
- 调用了一个阻塞式io
- 试图获得一个同步监视器,但该监视器正在被别的线程使用。
- 线程在等待某个通知
- 程序调用了线程的suspend()方法将线程挂起。
从阻塞变为就绪:
- 调用sleep()方法的线程经过了指定时间
- 线程调用的阻塞式io方法已经返回
- 线程成功获得了试图取得的同步监视器
- 线程正在等待某个通知时,其他线程发出了一个通知
- 处于挂起状态的线程被嗲用了resume()恢复方法
死亡
- run()或者call()方法执行完毕
- 线程抛出一个未捕获的异常或者错误
- 直接调用该线程的stop方法(容易死锁)
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来,他就拥有和主线程一样的地位。
调用线程的isAlive()方法可以返回线程的状态。新建和死亡状态返回false。
对于已经运行并死亡的线程调用
start()
方法会引起IllegalThreadException
四、控制线程
join线程
Thread提供了让一个线程等待另一个线程完成的方法--join方法。当某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
join()方法通常由使用线程的程序调用,以将大问题划分成小问题,每个小问题分配一个线程。当所有小问题处理完之后,再调用主线程进一步操作。
public class Test extends Thread{
//提供带参构造器
public Test(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws Exception{
new Test("新线程").start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"--"+i);
if(i==10){
Test t = new Test("又一个新线程");
t.start();
//main线程调用t线程的join方法,main必须等到t执行完成之后才能继续执行
t.join();
}
}
}
}
join方法的三种重载形式。
- join()
等待被join的线程执行完成。
- join(long millis)
等待被join的线程的时间最长为millis毫秒
join(long millis, int nanos)
后台线程
"Daemon Thread",也称为"守护线程"或者"精灵线程"。
jvm的垃圾回收线程就是后台线程。
如果所有的前台线程都已经死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)
方法即可。
isDaemon()
方法判断指点线程是否是后台线程。
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
setDaemon
必须在start
之前调用。
线程睡眠
让当前线程进入阻塞状态。
- static void sleep(long millis)
- static void sleep(long millis, int nanos)
线程让步
yield()
方法让当前线程暂停,进入就绪状态。
该方法只是让当前线程暂停一下,让系统的线程调度器重新调度一次。只有优先级与当前线程相同或者更改的处于就绪状态的线程才会获得执行的机会。
public class Test extends Thread{
//提供带参构造器
public Test(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<100;i++){
System.out.println(getName()+" "+i);
if (i==20){
Thread.yield();
}
}
}
public static void main(String[] args) {
Test t1 = new Test("高级");
//t1.setPriority(Thread.MAX_PRIORITY);
Test t2 = new Test("低级");
//t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
在多CPU并行的环境下,yield()方法的功能有时候并不明显。
sleep和yield方法的区别:
- sleep()不理会其他线程的优先级
- sleep()将线程转入阻塞状态,而yield()转让就绪状态
- sleep()方法声明抛出了
InterruptedException
,yield()方法没有声明抛出异常。 - sleep()方法有更好的可移植性,不建议使用yield()方法控制并发线程的执行。
改变线程的优先级
每个线程默认的优先级与创建它的父线程的优先级相同。main线程具有普通优先级。
setPriority(int newPriority),getPriority()。
参数范围为1~10.
Thread类有三个静态常量:
- MAX_PRIORITY(10)
- MIN_PRIORITY(1)
- NORM_PRIORITY(5)
不同系统的优先级不相同,不能很好的和Java的10个优先级对应,尽量避免直接指定优先级的数值。
五、线程同步
同步代码块
Java的多线程支持引入了同步监视器解决这个问题,使用同步监视器的通用方法是同步代码块。格式为:
synchronized(obj){
//同步代码块
}
obj是同步监视器。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
当同步代码块执行完成后,该线程释放同步监视器的锁定。
允许使用任何对象作为同步监视器。通常推荐可能被并发访问的共享资源充当同步监视器。
同步方法
synchronized修饰方法。
对于修饰的实例方法,无需显示指定同步监视器,同步方法的同步监视器是this
,也就是调用该方法的对象。
通过同步方法可以方便的实现线程安全的类,线程安全的类有如下特征:
-
该类的对象可以被多个线程安全的访问
-
每个线程调用该对象的任意方法之后都将得到正确的结果
-
每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
-
只对会改变竞争资源的方法进行同步。
-
如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本。线程安全版本和线程不安全版本。
释放同步监视器的锁定
程序无法显示释放,线程会在如下几种情况释放对监视器的锁定:
- 当前线程的同步方法、同步代码块执行结束。
- 同步方法、同步代码块遇到return、break
- 出现error或exception
- 程序执行了同步监视器对象的wait()方法
如下情况,线程不会释放同步监视器:
- 程序调用Thread.sleep()、Thread.yield()方法
- 其他线程调用该线程的suspend()方法将该线程挂起
同步锁
Java5开始。Lock对象。
Lock、ReadWriteLock是Java5提供的两个根接口,分别有ReentrantLock和ReentrantReadWriteLock实现类。
Java8新增了StampedLock类,在大多数场景中可以代替ReentrantReadWriteLock。
ReentrantReadWriteLock为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。
import java.util.concurrent.locks.ReentrantLock;
class Test{
private final ReentrantLock lock = new ReentrantLock();
public void m(){
//加锁
lock.lock();
try{
//代码块
}
finally{
lock.unlock();
}
}
}
lock提供了同步方法和同步代码块没有的其他功能。包括用于非块结构的tryLock()方法,试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的try(long, TimeUnit)方法。
死锁
Thread类的suspend方法很容易造成死锁,Java不再推荐使用该方法。
六、线程通信
传统的线程通信
wait()、notify()、notifyAll()
- 对于使用synchronized修饰的方法,该类的默认实例(this)就是同步监视器,可以在同步方法中直接调用这三个方法。
- 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象。必须使用该对象调用这三个方法。
使用condition控制线程通信
Lock对线保证同步时,可以使用Condition类来保持协调。
Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。
Condition类提供了如下三个方法:
- await()
- signal()
- signalAll()
使用阻塞队列(BlockingQueue)控制线程通信
Queue的子接口,主要用途作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者从中取出元素时,如果该队列已空,则该线程被阻塞。
提供的支持阻塞的方法:
- put(E e)
- take()
队列的方法:
- 在队列尾部放入元素:add(E e)、offer(E e)、put(E e),队列已满,这三个方法分别会抛出异常、返回false、阻塞队列
- 在头部删除并返回删除的元素,remove()、poll()、take(),队列已空,这三个方法分别会抛出异常、返回false、阻塞队列
- 头部取出但不删除元素,element()、peek(),队列已空,这两个方法会抛出异常、返回false
表头 | 抛出异常 | 不同返回值 | 阻塞线程 | 指定超时时间 |
---|---|---|---|---|
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
队头删除元素 | remove() | poll() | take() | poll(time,unit) |
获取、不删除 | element() | peek() | 无 | 无 |
七、线程池
Java8改进的线程池
Java5开始内建支持线程池,新增Executors工厂类产生线程池,静态方法:
- newCachedThreadPool()
创建一个具有缓存功能的线程池
- newFIxedThreadPoll(int nThreads)
创建一个可重用的、固定线程数量的线程池
- newSingleThreadExecutor()
创建一个只有单线程的线程池
- newScheduledThreadPoll(int corePoolSize)
指定延迟后执行线程任务
- newSingleThreadScheduledExecutor()
指定延迟
- ExecutorService newWorkStealingPool(int parallelism)
创建持有足够线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
- ExecutorService newWorkStealingPool()
相当于前一个方法的简化版,如果有4个cpu,相当于参数为4
Java8新增的ForkJoinPoll方法
八、线程相关类
ThreadLocal
提供了三个public方法:
- T get()
- void remove()
- void set(T value)
它将需要并发访问的资源复制多份,每个线程拥有一个资源,每个线程都拥有自己的资源副本。
不能代替同步机制。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间对共享资源的竞争;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争,不需要对多个线程同步。
通常建议:多个线程需要共享资源,以达到线程的通信功能,使用同步机制;仅仅需要隔离多个线程之间的共享冲突,使用ThreadLocal
包装线程不安全的类
Colletions提供的类方法:
- static <T> Collection<T> synchronizedCollection(Collection<T> c)
- static <T> List<T> synchronizedList(List<T> list)
- static <K,V> Map<K,V> synchronizedMap(Map<K,V> map)
- static <T> Set<T> synchronizedSet(Set<T> s)
- static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
- static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> m)
线程安全的集合类
java.util.concurrent
包下面提供了大量支持高效并发访问的集合接口和实现类。