概述
进程:
所有运行中的任务通常对应一个进程(process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程的三个特征:
1、独立性: 进程是系统中独立存在的实体,它可以用于自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2、动态性: 进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都不具备。
3、并发性: 多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
并发性与并行性:
1、并发性:(concurrency) 指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
2、并行性:(parallel) 指同一时刻,有多条指令在多个处理器上同时执行。
线程:
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的钱不资源。因为多个线程共享父进程里的全部资源,因此编程更加方便,但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
总结:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
单线程:安全性高,但是效率低
多线程:安全性低,效率高
主线程:执行主方法(main方法)的线程
单线程程序:java程序中只有一个线程,执行从main方法开始,从上到下依次执行。
硬盘:永久存储 ,只读存储器(Read-Only Memory)ROM
内存:临时存储, 随机存取存储器(Random Access Memory) RAM
多线程的优势:
1、进程之间不能共享内存,但线程之间共享内存非常容易。
2、系统创建进程时需要为该进程重新分配系统资源,但是创建线程代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
3、java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。
线程的创建和启动
一、通过继承Thread类来创建并启动多线程的步骤:
1、定义Thread类的子类,并重新该类的run()方法,该方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
2、创建Thread子类的实例,即创建了线程对象。
3、调用线程对象的start()方法来启动该线程。
public class FirstThreadDemo extends Thread {
private int i;
public void run(){
for (;i<100;i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20){
new FirstThreadDemo().start();
new FirstThreadDemo().start();
}
}
}
}
注意:线程开启不一定立即执行,由cpu调度执行
二、实现Runnable接口来创建并启动多线程的步骤:
1、定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2、创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3、调用线程对象的 start()方法来启动该线程。
public class SecondThreadDemo implements Runnable {
private int i;
@Override
public void run() {
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 20){
SecondThreadDemo threadDemo = new SecondThreadDemo();
new Thread(threadDemo,"新线程1").start();
new Thread(threadDemo,"新线程2").start();
}
}
}
}
三、使用Callable来创建并启动线程的步骤:
1、创建Callable接口的实现类,并实现call()方法,该方法将作为线程执行体,且该call()方法有返回值,在创建Callable实现类的实例。
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3、使用FutureTask对象作为Thread对象的target创建并启动新线程。
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class ThirdThreadDemo {
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
}
return i;
});
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
if(i == 20){
new Thread(task,"有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值:"+task.get());
}catch(Exception e){
e.printStackTrace();
}
}
}
创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口实现Callable接口归为一种方式,这种方式与继承Thread方式之间的主要差别如下
采用实现Runnable。Callable接口的方式创建多线程的优缺点:
1、线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
2、在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
3、劣势是,编程稍微复杂,如果需要访问当前线程,则必须是呀Thread.currentThread()方法,
采用继承Thread类的方式创建多线程的优缺点:
1、劣势是,因为线程类已经继承了Thread类,所以不能在继承其他父类。
2、优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
总结: 一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
线程的声明周期
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead) 5种状态。
新建:
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态。
就绪:
当线程对象调用了start()方法之后,该线程处于就绪状态。
运行:
如果处于就绪状态的线程获得了cpu资源,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞:
当发生如下情况时,线程将会进入阻塞状态
1、线程调用sleep()方法主动放弃所占用的处理器资源。
2、线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
3、线程视图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4、线程在等待某个通知。
5、程序调用了线程的suspend()方法将线程挂起,但是这个方法容易导致死锁,所以应该尽量避免使用该方法。
当发生如下情况时,可以解除阻塞,线程重新进入就绪状态
1、调用sleep()方法的线程经过了指定时间。
2.线程调用的阻塞式IO方法已经返回。
3、线程成功地获得了视图取得的同步监视器。
4、线程正在等待某个通知时,其他线程发出了一个通知。
5、处于挂起状态的线程被调用了resume()恢复方法。
死亡:
线程会如下三种方式结束,结束后处于死亡状态
1、run()或call()方法执行完成,线程正常结束。
2、线程抛出一个未捕获的Exception或Error。
3、直接调用该方法的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用、
**注意:**不要对处于死亡状态的线程调用start()方法,该程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的,这都会引发IllegalThreadStateException异常。
控制线程
线程等待
让一个线程等待另一个线程完成,当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完为止。
后台线程
在后台运行的线程,它的任务是为其他的线程提供服务,这种线程被称为后台线程(Daemon Thread),又称为“守护线程”或“精灵线程”。jvm的垃圾回收线程就是典型的后台线程。如果所有的前台线程都死亡了,后台线程会自动死亡。setDaemon(true)方法
前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
注意: 前台线程死亡后,jvm会通知后台线程死亡,但从它接收指令到做出响应,需要一定的时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。
线程睡眠
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
线程让步
就是调用Thread类的yield()方法让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。让系统的线程调度器重新调度一次。
改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会,每个线程默认的优先级都与创建它的父线程的优先级相同。Thread提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。
线程同步
线程安全
1、同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
需要同步操作的代码
}
//同步锁就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
public class DrawThreadDemo extends Thread {
private Account account;
private double drawAmount;
public DrawThreadDemo(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
public void run() {
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println(this.getName() + "取钱成功!:" + drawAmount);
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t 余额为:" + account.getBalance());
} else {
System.out.println(getName() + "取钱失败,余额不足");
}
}
}
}
2、同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() { System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
//使用死循环,让卖票操作重复执行
while(true){
payTicketStatic();
}
}
/*
静态的同步方法
锁对象是谁?
不能是this
this是创建对象之后产生的,静态方法优先于对象
静态方法的锁对象是本类的class属性-->class文件对象(反射)
*/
public static /*synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
/*
定义一个同步方法
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl()
也是就是this
*/
public /*synchronized*/ void payTicket(){
synchronized (this){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("run:"+run);//run:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
释放同步监视器的锁定
程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放同步监视器:
1、当线程的同步方法、同步代码块执行结束。
2、当前线程在同步代码块、同步方法中遇到bread、return终止了该代码块、该方法的继续执行。
3、当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时。
4、当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停。
3、同步锁(Lock)
在实现线程安全控制中,比较常用的是ReentrantLock(可重入锁)。使用该lock对象可以显示低加锁、释放锁。
通常建议使用finally块来确保在必要时释放锁。
//定义锁对象
private final ReentrantLock lock = new ReentrantLock()
4、死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现,一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
线程通信
1、传统的线程通信
对于使用synchronize修饰的同步方法,因为该类默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
对于使用synchronize修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用者三个方法。
wait()
notity()
notityAll()
2、使用Condition控制线程通信
await()
signal()
signalAll()
3、使用阻塞队列控制线程通信
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
put() 放元素
take() 取元素
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,
有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
等待唤醒:线程之间的通信
void wait()
在其他线程调用此对象的 notify()方法或 notifyAll() 方法前,导致当前线程等待。
void notify()
唤醒在此对象监视器上等待的单个线程。
void nottifyAll()
唤醒在此对象监视器上等待的所有线程。
/*
等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法
Obejct类中的方法
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify()
唤醒在此对象监视器上等待的单个线程。
会继续执行wait方法之后的代码
*/
public class Demo01WaitAndNotify {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//一直等着买包子
while(true){
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("告知老板要的包子的种类和数量");
//调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子已经做好了,开吃!");
System.out.println("---------------------------------------");
}
}
}
}.start();
//创建一个老板线程(生产者)
new Thread(){
@Override
public void run() {
//一直做包子
while (true){
//花了5秒做包子
try {
Thread.sleep(5000);//花5秒钟做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
//做好包子之后,调用notify方法,唤醒顾客吃包子
obj.notify();
}
}
}
}.start();
}
}
线程池
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
public class Demo01ThreadPool {
public static void main(String[] args) {
//1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
//线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行
//4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
es.shutdown();
es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
}
}
public class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
}
}