- 什么是进程?
在多任务操作系统之中,一个后台正在运行的应用就是一个进程。
通俗定义而言,就是一个应用程序就是一个进程。
- 进程与线程的关系?
一个进程需要完成多个任务,这时候利用线程进行任务操作,可以极大提高CPU的利用率。
一个进程至少有一个线程,一个线程只能属于一个进程。
- 线程的生命周期。
线程按照其创建、运行以及结束几个阶段,划分为六个生命周期。分别为:
新建状态:jvm为创建的线程对象分配推空间和相关资源,当线程被创建完成时即为新建状态;
可运行状态:该状态实际包含两种状态,分别是就绪状态和运行状态。当创建的线程对象执行start方法时,则会进入就绪状态;当进入就绪状态的线程争抢到cpu资源时,会执行run方法,进入运行状态。
阻塞状态:正在运行的线程对象由于缺乏相应的资源,无法继续执行后续任务,此时会让出cpu进入阻塞状态,当获取到需要的资源时,会重新进入就绪状态争抢cpu资源。线程阻塞原因较为常见的有:
- 当前正在运行的线程,需要访问同步锁资源,当同步锁资源被其他线程占有,只能等待其他线程释放资源才能执行后续任务;
- 当前线程发出I/O请求。
等待状态:正在运行的线程通过调用无时间参数限制的方法后,进入等待状态,只有被重新唤醒才能进入就绪状态。比如线程调用wait()、join()等方法,从而进入等待状态,只有其他线程通过notify或者notifyAll方法唤醒后才能进入就绪状态(join方法只能等待插入的线程执行完毕之后才能进入就绪状态)。
定时等待状态:与定时状态的区别在于,其含有时间参数,在指定的时间参数结束后或者其他线程唤醒后,线程自动进入就绪状态。
终止状态:线程的run方法或者call方法执行完毕之后或者捕捉到程序异常或者错误时,线程进入终止状态,即线程死亡。
- 单线程程序与多线程程序。
单线程程序:一般只含有主程序,即一个程序按照顺序运行。较为脆弱和效率过低。
多线程程序:将一个程序划分不同任务给线程,每一个线程都能独立完成任务,以此提高cpu的利用效率。
前台和后台:每一个运行的线程都默认为前台,前台线程运行结束以后,后台程序自动结束。前台线程也可以转化为后台线程。
线程的优先级:线程抢占cpu资源的方式一般有两种。一为通过时间片原则抢占cpu资源,即划分执行时间,每一个线程在执行时间结束后释放cpu资源;二为通过抢占式的方法,各线程竞争cpu资源,该方式也为jvm默认的线程调度方式。在抢占式方法中,可以通过设置线程的优先级提高线程的抢占率,但并不是优先级高的一定能够抢占到cpu资源。
- 创建线程对象的方法。
- Thread。通过继承Thread类,重写run方法,定义一个子类。通过该子类创建实例对象,即创建了线程对象。
- Runnable。Runnable是一个接口,构造该接口的实现类,重写run方法,实例化对象,再将runnable子类实例化的对象作为参数传入Thread类的构造方法之中。
- Callable。该类也为接口类,通过构造该类的实现类,重写call实现线程创建。调用时,实例化子类对象,通过Future类封装子类对象,再将封装后的FutureTask对象作为参数传入Thread的构造方法之中。
- 通过线程池创建以上三种线程对象。实际上,线程类型同以上三种,只是线程池提供了对线程对象的管理,使线程更加安全、高效的运行。
- 各种方法之间的区别。
Thread:该继承方法是线程实现的基础,但由于java中只支持单继承,若一个类已经有父类,需要在使用线程时,便无法通过此种方式创建线程;
Runnable:该类为接口类,java中支持多实现,解决了已有父类无法创建线程的问题。
Callable:该类为接口类,在runnable接口的基础上,实现了对线程执行结果的获取,即线程执行结束之后可以返回一个执行结果。
- 线程同步。
- 线程安全:当多个线程对同一个资源进行请求时,由于线程的延迟,会导致被请求资源出现错误,如数据不一致、数据出现不应有的改变等。
- 实现线程安全的方法:
- 同步代码块:由于线程安全问题是由多个线程共享同一资源而导致的,要解决线程安全问题,必须保证任意时刻,共享资源的代码块只能被一个线程访问。
- 实现方式:使用synchronized关键字修饰共享资源的代码块。实质上为将共享资源的代码块上同步锁,当有线程在访问时,锁定程序代码块,只有访问的线程执行结束后,才能重新释放代码块。
- 基本格式为:synchronized(lock){...;//资源代码块}
- Lock为锁对象,可以是任意类型。但是不同线程共享的锁对象必须保持一致,如此才能使锁的标志位对所有线程而言都是一样的。
- 同步方法:在方法的修饰符后面使用synchronized关键字,可以将方法进行锁定,一次只能允许一个线程访问该方法。此时,该方法的锁对象是方法所在的对象本身;若为静态方法,锁对象为方法所在的类的class对象。
优点:解决了多个线程同时访问共享数据时的线程安全问题。
缺点:线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
- 同步锁。
背景:同步代码块和同步方法的使用是一种封闭的锁机制,使用简单,也能较好的解决线程安全问题,但是该方法无法中断等待锁的进程不在等待,也无法通过轮询的方式得到锁,需要一直等待才能得到锁。
Lock锁:jdk5开始时引入,功能与synchronized相似,但是可以让线程持续获取同步锁失败后返回,不再继续等待,同时使用也更加灵活。
Lock是一个接口类,其有多个实现类,其中最为常用的有ReentrantLock类,常用的方法有:
Lock():在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法。Lock不会自动释放锁,哪怕程序异常也不会自动释放,只能手动释放锁。
tryLock() :用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败。相比于 lock(),这样的方法显然功能更强大,我们可以根据是否能获取到锁来决定后续程序的行为。
Unlock():释放锁。一般使用try-catch模块对程序异常进行捕获,并且在finally中使用该方法释放锁,确保锁一定会在程序执行完后被释放。
lockInterruptibly():这个方法的作用就是去获取锁,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程持有),那么当前线程便会开始等待,除非它等到了这把锁或者是在等待的过程中被中断了,否则这个线程便会一直在这里执行这行代码。一句话总结就是,除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止。
顾名思义,lockInterruptibly() 是可以响应中断的。
- 线程通信。
背景:
若一个线程执行run方法时,对于不同的方法需要获取不同的锁,并且要在执行的方法之中获取下一步的锁才能继续执行,此时若有多个线程同时执行run方法,分别获得不同方法的锁,此时,在每个线程执行完当前方法后,想要继续执行后续方法,获取锁时都是锁定状态,只能不断等待下去,便造成了线程之间的死锁问题。
线程通信:
解决死锁问题,可以通过线程之间通信,释放彼此需要的锁来进行。线程通信即通过信号量机制实现。
- 信号量机制。
意义:实现线程通信,解决线程堵塞问题。
实际应用:实现资源能被有限个线程使用。
特点:
信号量机制不同于互斥锁,其可以指定某一个资源能够被访问的线程最大数量,当有一个线程访问该资源时,信号量减一,当线程完成资源访问释放时,信号量加一,故信号量也可看作一个计数器。
Java中的信号量对象:Semaphore。其构造时可以传入一个整型参数,表示对应资源可接受线程的最大访问数量。
CountDownLatch:实现线程等待,即需要指定个数的线程到达某一个点时才进行下一步操作。
- 线程池。
优点:
1.降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2.提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3.方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
4.更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
常用线程池:
- Executor(通用线程池):
时间:jdk5被引入,在java.util.concurrent包中,其为接口类,常用的子接口有ExecutorService,通过该接口可以很好的进行线程管理。
- Executor实现线程管理池的主要步骤:
(1)创建线程实现类对象。
(2)使用Executor线程执行器类创建线程池。
(3)使用ExecutorService执行器服务类的submit()方法将线程实现类对象提交到线程池管理。
(4)线程任务执行完后,使用shutdown()方法关闭线程池。
- CompletableFuture(函数式异步编程辅助类):
背景:jdk8时引入,实现了Future接口和CompletionStage接口
(jdk8时引入的线程任务完成接口),对Future接口进行了强大的扩展,同时简化了异步编程的复杂性,其实际上是为了解决callable实现接口时,使用FutureTask进行封装的不足。
实际代码演示:
- 创建线程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Threads extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"为通过继承Thread所创建的线程!");
}
}
class Runnables implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"为通过实现Runnable所创建的线程!");
}
}
class Callables implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"为通过实现Callables所创建的线程!");
return 0;
}
}
public class CreateThreads {
public static void main(String[] args){
Threads threads = new Threads();
threads.setName("线程一");
threads.start();
Runnables runnables = new Runnables();
Thread temp = new Thread(runnables);
temp.setName("线程二");
temp.start();
Callables callables = new Callables();
FutureTask<Integer> future = new FutureTask<Integer>(callables);
Thread temps=new Thread(future);
temps.setName("线程三");
temps.start();
try {
int ans=future.get();
System.out.println("Callable返回的结果为:"+ans);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
2.同步代码块
/*
* 该代码为同步静态资源和方法,需要注意,同步块中的锁对象需要为静态变量
* 保证线程访问资源都共用同一个锁对象。
* 若共享成员变量,则创建线程时,使用同一个实体类对象作为参数。
* */
class ThreadTest extends Thread{
public static int sum=20;
public static int ans=20;
private static Object ob = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
while(sum>0) {
synchronized (ob) {
if(sum>0) {
try {
sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(currentThread().getName()+"消耗资源"+sum--);
}
}
}
while(ans>0) {
try {
sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
this.Spend();
}
}
public static synchronized void Spend() {
if(ans>0) {
System.out.println(currentThread().getName()+"通过同步方法消耗资源"+ans--);
}
}
}
public class SynchronizedBlock {
public static void main(String[] args) {
ThreadTest test1 = new ThreadTest();
test1.setName("线程一");
ThreadTest test2 = new ThreadTest();
test2.setName("线程二");
ThreadTest test3 = new ThreadTest();
test3.setName("线程三");
// ThreadTest test4 = new ThreadTest();
// test4.setName("线程四");
//
// ThreadTest test5 = new ThreadTest();
// test5.setName("线程五");
test1.start();
test2.start();
test3.start();
// test4.start();
// test5.start();
}
}
运行结果为:
3.lock锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Runnablel implements Runnable{
private int socre = 20;
private Lock lock = new ReentrantLock();
@Override
public void run() {
// TODO Auto-generated method stub
while(this.socre>0) {
lock.lock();
try {
if(this.socre>0) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"正在消耗资源"+this.socre--);
}
} catch (Exception e) {
// TODO: handle exception
}
finally {
lock.unlock();
}
}
}
}
public class RunnableLock {
public static void main(String[] args) {
Runnablel runnables = new Runnablel();
Thread test1 = new Thread(runnables,"线程1");
Thread test2 = new Thread(runnables,"线程2");
Thread test3 = new Thread(runnables,"线程3");
test1.start();
test2.start();
test3.start();
}
}
运行结果
4.线程通信
import java.util.concurrent.Semaphore;
class Runnablec implements Runnable {
private int resource = 20;
//创建信号量
private Semaphore A = new Semaphore(1);
private Semaphore B = new Semaphore(1);
@Override
public void run() {
// TODO Auto-generated method stub
while(resource>0) {
try {
A.acquire();
if(resource>0) {
Thread.sleep(1000);
this.display();
B.acquire();
this.showInfo();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
B.release();
}
}
}
public void showInfo() {
System.out.println(Thread.currentThread().getName()+"获取了信号量B释放了信号量A");
this.A.release();
}
public void display() {
System.out.println(Thread.currentThread().getName()+"获取了信号量A消耗资源"+this.resource--);
}
}
public class ThreadCommunication {
public static void main(String[] args) {
Runnablec rc = new Runnablec();
Thread t1 = new Thread(rc,"线程1");
Thread t2 = new Thread(rc,"线程2");
Thread t3 = new Thread(rc,"线程3");
t1.start();
t2.start();
t3.start();
}
}
运行结果部分截图:
5.线程池使用
参考博客:Java 多线程:彻底搞懂线程池_孙强 Jimmy的博客-优快云博客_java 多线程池
补充说明:
- 创建和使用线程时,容易引发oom问题,故需要对线程进行控制,尤其对于线程池而言,直接使用Executors创建线程容易引发以上问题;
- 针对不同的并发场景,使用不同的线程池进行线程管理可极大节约资源。