进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全.并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
public class MyThread extends Thread { public void run() { System.out.println("MyThread.run()"); } } MyThread myThread1 = new MyThread(); myThread1.start();
案例
上面案例的执行顺序是这样的:
每个线程都有自己的栈空间,共用一块堆内存。由一个线程调用的方法那么这个方法也会执行在这个线程里边。
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。
打开API也可以看到很多关于Thread类的方法(关于API的下载关注公众号:小小李童鞋回复JavaAPI获取)
设置和获取线程名称
可以用对象调用它的setName方法设置名称。
线程的休眠可以调用它的sleep方法
每隔一秒执行一次循环
线程阻塞
其实就是所有比较消耗时间的操作,比如说读取文件,接收用户输入数据等
线程中断
当主线程执行完毕,就调用子线程的中断方法,子线程捕获到异常然后return掉,子线程就被杀死了。
守护线程
定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3. 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
4. 在 Daemon 线程中产生的新线程也是 Daemon 的。
5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
7. 生命周期:守护线程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出
子线程和上边的继承Runnable接口一样,这里改动就是将子线程设置为守护线程。
举例:售票窗口
当new了三个线程同时进行卖票操作就出现了线程不安全问题
通过同步代码块改进后的代码
三个线程看同一把锁,线程安全得到了解决,但是效率比较低了
反例,如果每个线程看自己的锁,那么依然是线程不安全的
通过给方法设置关键字synchronized来保证线程安全,改进后的代码,把上边的if里边的语句单独抽出来为一个方法
如果这里创建三个对象那么这个程序就错误了,因为是三个对象分别卖他们的十张票,
所以,切记我们用的是同一个对象
上面两个都是隐式锁,自己完成锁定解锁过程。
显示锁需要手动解锁,锁上,这个过程
显示锁有一个参数:
两个线程互相调用对方,导致双方都被锁上,看下边这个案例:
package thread; public class Demo { public static void main(String[] args) { //线程死锁 Culprit c = new Culprit(); Police p = new Police(); new MyThread(c,p).start(); c.say(p); } static class MyThread extends Thread{ private Culprit c; private Police p; MyThread(Culprit c,Police p){ this.c = c; this.p = p; } @Override public void run() { p.say(c); } } static class Culprit{ public synchronized void say(Police p){ System.out.println("罪犯:你放了我,我放了人质"); p.fun(); } public synchronized void fun(){ System.out.println("罪犯被放了,罪犯也放了人质"); } } static class Police{ public synchronized void say(Culprit c){ System.out.println("警察:你放了人质,我放了你"); c.fun(); } public synchronized void fun(){ System.out.println("警察救了人质,但是罪犯跑了"); } } }
两个线程在合作的时候,如果不加锁机制,可能会发生数据错乱,看下边这个例子,其中name 和taste就发生了错误,因为在线程休眠的期间另一个线程可能就完成了操作。
另一个问题,加了这个锁机制,因为只是给它做饭的步骤那里加的,会出现一个问题,厨师可能一直在那里做饭,或者服务员一直在那里端饭,理想状态是一人一次
最终代码改进。设置一个flag,为true表示厨师可以做饭,当做完饭的时候改为false,并唤醒当前沉睡的线程自己进入休眠,服务员里的get方法也一样。
package thread; public class Demo12 { public static void main(String[] args) { //多线程通信 生产者与消费者问题 Food f = new Food(); new Cook(f).start(); new Waiter(f).start(); } //厨师 static class Cook extends Thread{ private Food f; public Cook(Food f) { this.f = f; } @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ f.setNameAndTaste("老干妈小米粥","香辣味"); }else { f.setNameAndTaste("煎饼果子","甜辣味"); } } } } //服务员 static class Waiter extends Thread{ private Food f; public Waiter(Food f) { this.f = f; } @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } } } //食物 static class Food{ private String name; private String taste; //true表示可以生产 boolean flag = true; public synchronized void setNameAndTaste(String name,String taste){ if(flag){ this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void get(){ if(!flag){ System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste); flag = true; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
关于线程的六种状态详细介绍可以看这篇文章
线程生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
程序运行结束,线程自动结束。
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while
循环是否退出,代码示例:
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
使用 interrupt()方法来中断线程有两种情况:
public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕获到异常之后,执行 break 跳出循环 } } } }
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。(join线程其实是一个VIP线程插队的过程)
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
System.out.println(Thread.currentThread().getName() + "线程运行开始!"); Thread6 thread1 = new Thread6(); thread1.setName("线程 B"); thread1.join(); System.out.println("这时 thread1 执行完毕之后才能执行主线程");
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
Runnable 与 Callable
接口定义
//Callable接口
public interface Callable {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
Callable使用步骤
Runnable 与 Callable的相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
Runnable 与 Callable的不同点
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
具体案例:
通过运行结果可以看到两个线程在执行的过程中因为子线程没有调用get方法获取返回值所以主线程不会被阻塞,因此两个线程抢占的时间片是相等的。但是下边通过给子线程调用get方法获取callable的返回值此时主线程会被阻塞。优先执行子线程,子线程执行完毕主线程才会继续执行。
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处1 降低资源消耗。2 提高响应速度。3 提高线程的可管理性。
通常设置这样一个线程池,有定长的 还有不定长的
定长的
创建了一个线程池,刚开始线程池里的四个线程都是空闲状态,随着任务列表里加入了任务,空闲的线程会去领任务,同时将状态改为忙碌的状态。当A把它的任务执行完成可以接收任务列表新的等待的任务,那么会造成D线程一直出于“饥饿状态”,加入你有500个任务那么创建800个容量的线程池可能会造成资源浪费,所以合理设计线程池是必要的。
当四个线程都领到任务处于忙碌的状态时,任务列表中的任务会处于等待状态,但是对于非定长的线程池,他会自己再扩充容量,以便完成任务列表中的任务,经过一段时间后,他会把一些一直都处于空闲的线程在释放掉。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
线程池的组成
一般的线程池主要分为以下 4 个组成部分:
/** * 缓存线程池. * (长度无限制) * 执行流程: * 1. 判断线程池是否存在空闲线程 * 2. 存在则使用 * 3. 不存在,则创建线程 并放入线程池, 然后使用 */ ExecutorService service = Executors.newCachedThreadPool(); //向线程池中 加入 新的任务 service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo14 { /*定长线程池 长度是指定的线程池 加入任务后的执行流程 1 判断线程池是否存在空闲线程 2 存在则使用 3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用 4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程 **/ public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } }); } }
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo15 { /*单线程线程池 执行流程 1 判断线程池的那个线程是否空闲 2 空闲则使用 3 不空闲则等待它空闲后再使用 **/ public static void main(String[] args) { ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } }); service.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } }); } }
package thread; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Demo16 { /*周期任务 定长线程池 执行流程 1 判断线程池是否存在空闲线程 2 存在则使用 3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用 4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程 周期性任务执行时 定时执行 当某个任务触发时 自动执行某任务 **/ public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2); //定时执行一次 //参数1:定时执行的任务 //参数2:时长数字 //参数3:2的时间单位 Timeunit的常量指定 /* scheduledExecutorService.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } },5, TimeUnit.SECONDS); //5秒钟后执行*/ /* 周期性执行任务 参数1:任务 参数2:延迟时长数字(第一次在执行上面时间以后) 参数3:周期时长数字(没隔多久执行一次) 参数4:时长数字的单位 * **/ scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"锄禾日当午"); } },5,1,TimeUnit.SECONDS); } }
Lambda表达式是一个函数式接口,只关注结果
用函数式编程:
再看一个案例:
两个数求和,使用Lambda表达式改进后
其实也就是删除了创建对象的过程。
为什么使用Lambda表达式:
1避免匿名内部类定义过多
2可以让代码看起来更简洁
3去掉了一堆没有意义的代码,只留下核心的逻辑
以上内容是关于java多线程的一些基本知识点,后续请关注公众号(小小李童鞋)获取多线程面试考点。