Java之多线程
程序员文章站
2023-12-23 15:31:34
...
什么是线程
线程是进程的组成部分,线程可以拥有自己的堆栈,自己的程序计数器及自己的局部变量,但是线程不能拥有系统资源,它与其父进程的其他线程共享进程中的全部资源。
一个进程中可以包含多个线程,但是至少要包含一个线程,即主线程
特点
- 同一进程下的不同线程的调度不由程序控制
- 线程独享自己的堆栈程序计数器和局部变量
- 两个线程将并发执行(本质上是宏观上并行、微观上串行)
进程和线程的区别
- 线程是进程的执行单元,线程是操作系统能够进行运算调度的最小单元
- 线程和进程的关系是“多对一”
- 线程可以独享自己的堆栈、程序计数器和局部 变量;但线程必须与其父进程的其他线程共享代码段、数据段、堆空间等系统资源。
实现线程的三种方法
Thread( )类及其派生类
- 使用Thread类或者使用一个派生自Thread类的类构建一个线程
- 继承Thread的子类在实例时,使用了上溯造型
- 需要new一个线程对象,在调用该对象的start()方法
public class thread {
public static void main(String[] args) {
Thread t= new Child();
t.start();
}
}
class Child extends Thread{
public void run() {
System.out.print("I am running");
}
}
实现Runnable接口
- 执行时也要new一个线程对象,但是需要进行参数传入,传入的的是实现了Runnable接口的实例对象,以构建线程对象
-
推荐使用实现Runnable接口的方法来进行多线程编程
因为java不支持多继承,如果一个子类继承了Thread类,就不能继承其他类了,使用实现Runnable接口的类,既可以实现一个线程的功能, 又可以更好的复用其他类的属性和方法。
public class thread {
public static void main(String[] args) {
Child child = new Child();
Thread thrd = new Thread(child);
thrd.start();
}
}
class Child implements Runnable{
public void run() {
System.out.print("I am running");
}
}
实现Callable接口
Callable的定义
public interfacec Callable<V> {
V call() throws Exception;
}
1.Callable是一个泛型接口,call()方法类似run()方法,是线程所关联的执行代码,但是call()方法具有返回值,如果call()得不到返回值就会抛出一个异常,Runnable的run()方法不会抛出异常
2.获得call()的返回值,可以通过Future接口来获取。Future接口的get方法用于获取call()的返回值,它会发生阻塞,知道call()返回结果。
public class thread {
public static void main(String[] args) {
Child<String> child = new Child<String>();
//FutureTask本身实现了Future和Runnable接口,既可以把FutureTask的实例传入Thread中,在一个新的线程中
执行,同时又可以从FutureTask中通过get方法获取到任务的返回结果
FutureTask<String> future = new FutureTask<String>(child);
Thread t = new Thread(future);
t.start();
try {
System.out.print(future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Child<Srring> implements Callable<String> {
@Override
public String call() throws Exception {
return "I am calling";
}
}
线程的状态
线程有5个状态:新建、就绪、运行、死亡、阻塞
新建(new)
- new一个线程时,该线程处于新建状态
- 此时线程是一个在堆中分配了内容的静态的对象
就绪(Runnable)
- 调用start( )方法后,处于就绪状态
- JVM会为该线程创建方法调用栈和程序计数器
- 线程处于运行池中,等待JVM的调度
运行(Running)
- 处于就绪状态的线程获得CPU开始运行run方法,进入运行状态
- 对于抢占式策略的操作系统,系统会为每个可执行的线程分配一个时间片。
当时间片用尽后系统会剥夺该线程所占有的处理器资源,从而让其他线程获得占有CPU的机会,
此时线程从运行态转为就绪态,继续等待调度
阻塞(Blocked)
遇到如下情况,线程进入阻塞状态
- 调用sleep、join方法
- 调用一个阻塞式IO方法
- 试图获得一个同步监视器,但是该监视器正在被其他线程持有
- 等待某个notify通知
- 程序调用了suspend方法将该线程挂起
死亡(Dead)
遇到如下情况,程序进入死亡状态
- run方法执行完
- 抛出一个异常或者错误,而未被捕获
- 调用stop方法
线程的控制
join
A调用B(处于就绪)的join方法,A被阻塞,B进行运行,B执行完后才执行A
join( );
join(long millis);//最长等待millis毫秒
join(long millis,int nanos;//最长等待millis毫秒加上nanos微秒
sleep
A调用sleep方法,A进入阻塞状态
sleep(long millis);
sleep(long millis,long nanos);
yield
A调用yield方法,A暂时放弃CPU,A不会被阻塞,而是进入就绪状态,等待调度
优先级
- 默认下,线程的优先级和创建该线程的父线程的优先级相等
- priority的取值范围1~10
- 三个静态常量
MAX_PRIORITY:最高优先级,值为10
MIN_PRIORITY:最低优先级,值为1
NORM_PRIORITY:普通优先级,值为5
setPriority(int priority);//设置优先级
getPriority();//返回优先级
终止线程
不建议使用stop和suspend
- 一旦调用stop方法,工作线程将派出一个ThreadDeath的异常,会导致run方法结束运行,结束点不可控
- 会释放掉线程所持有的锁,加锁的目的是保持数据的一致性,一旦停止,被保护的数据可能会出现不一致的情况
其他线程获得同步锁后可能进入临界区使用这些被破坏的数据,造成奇怪错误产生 - 使用suspend,可能会产生死锁
调用suspend()方法,虽然线程被阻塞,但是锁并没有被释放,在等待恢复的过程中,如果能够恢复这个被suspend的线程的线程需要访问被该线程锁定的临界资源,那便会产生死锁。
终止线程的方法
使用退出标志
当线程处于运行状态时,设置退出标志的方法安全结束该线程
public class thread {
public static volatile boolean exit = false;
public static void main(String[] args) {
new Thread() {
public void run() {
while (!exit) {//死循环
System.out.println("1");
}
System.out.println("Exit");
};
}.start();
new Thread() {
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
exit = true;
};
}.start();
}
}
使用interrupt方法
当一个线程被阻塞而无法正常运行时,使用interrupt()方法,可以使得阻塞的线程抛出一个异常使其退出阻塞状态,并catch住该异常从而安全结束该线程。
public class thread {
public static void main(String[] args) throws InterruptedException {
Thread thr = new Thread() {
public void run() {
System.out.println("Thread is running!");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thr.start();
Thread.sleep(1000);
thr.interrupt();
}
}
线程的同步(三种)
同步代码块
- 参数obj是一个引用类型的对象,也就是同步监视器,可以是任何引用类型的对象,即Object类的对象
- 任何时刻只能有一条线程获得对同步监视器的锁定
- 代码块执行完,释放同步监视器的锁定
synchronized(ob){
//需要同步的代码块
}
同步方法
- 使用关键字synchronized来修饰某个方法
- 同步方法中默认的同步监视器是this,该对象本身
同步锁
- 使用同步锁机制对临界资源保护时需要定义一个Lock类型的对象
- 通常使用Lock类的子类ReentrantLock(可重入锁)的对象来进行加锁和解锁的操作
private final ReentranLock lock = new ReentrantLock();
lock.lock(); //加锁
try{
//访问临界资源
}
finally{
lock.unlock();//释放锁
}
synchronized和Lock的区别
- 锁机制不同
- 用法不同
- 性能不同
Lock的功能更加强大,Lock能完成synchronized实现的所有功能
守护线程(Daemon Thread)
- 守护线程又称后台线程或服务线程,主要用来后台为用户线程提供服务
- 守护线程依赖于用户线程的执行状态。当一个程序中的所有用户线程执行完后JVM会退出,此时无论守护线程是否执行完都会结束掉。相反,如果还存在未执行完的用户线程,则JVM不会退出,守护线程也会继续执行
- JVM中的GC(垃圾回收器),就是一个经典的守护线程。当JVM运行时,GC会一直运行, 实时监控和管理着系统中可被回收的资源。
Thread t = new Thread();
t.setDaemon(true);
t.start();
线程的协调机制
- wait( )和notify( )方法
- await( )和signal( )方法
- BlockingQueue阻塞队列方法
- PipedInputStream/PipedOutputStream方法