多线程技术概述
内容较多,目录在右手边→
进程:指在内存中运行的应用程序。每一个运行中的程序在内存中都有自己的独立内存空间。
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,一个进程至少有一个线程,线程其实是在进程基础上的进一步划分,一个进程启动后,里面的执行路径又可以分为多个线程。
线程调度
分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。(轮流使用,让内存更合理的分配使用)。
抢占式调度:让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java就是使用的这种抢占式调度。(时间偏)
CPU使用抢占式调度模式在多个线程间进行高速切换,对于CPU的一个核心而言某个时刻只能执行一个线程。而CPU的在多个线程间切换速度相对于对我们的感觉要快,看起来就是在同一个时刻运行。其实多线程程序不能提高程序运行速度,但是能提高程序的运行效率,让CPU的使用率更高。
同步与异步:
同步:排队执行,效率低但是安全。
异步:同时执行,效率高但是数据不安全。
并发与并行:
并发:两个或多个事件在同一个时间段内发生。
并行:两个或多个事件在同一个时刻发生(同时发生)。
继承Thread
需要继承Thread类,重写run方法。
在main方法里运行的是主要线程,之后开启的都是分支线程。在主线程调用分支线程的时候需要使用分支对象.start()方法。
在启动时候线程谁先谁后结束是不确定的,所以会出现有的先执行有的后执行。
流程图示意:
另外,每个线程都有自己的栈空间,公用一份堆内存。由一个线程调用的方法,这个方法也是在这个线程里执行。
实现Runnable
需要重写run方法,这一块相当于写好了一个给线程执行的任务。
之后还需要使用Thread类。
此方式执行的多线程相当于将Runnable类创建了一个任务的对象,然后将其放入创建的Thread线程中执行。
优势:
Java不允许多继承,但是允许多实现,可以实现多个任务。线程池也不能接收Thread类型的线程。
当然Thread也是有好用的方法。例如可以使用匿名内部类进行简单的多线程操作,方便且占用内存少。
Thread类
对于Thread类有很多构造方法。
常用方法:
getName() 获取名称
getId() 获取id标识符
getPriority() 获得优先级
setPriority() 设定优先级
start() 开始线程
stop()方法不能用来停止线程。应该使用一些判断来让线程自己return来结束。
sleep(long millis, int nanos) 让线程休眠。休眠多少毫秒,或者纳秒。
setDaemon(boolean) 判断是否是守护线程。(守护线程和用户线程)。用户线程必须全部结束才能让程序结束。守护线程在用户线程结束了就会停止。
设置和获取线程名称:
传参的时候不传名称就是默认的Thread-i。
Thread.currentThread()方法可以调用当前的线程,返回值就是当前的线程。
线程的休眠
可以指定时间休眠。实例为输出一次循环休眠1000ms,即一秒输出一次。
线程阻塞(耗时操作)
指的是常见的一些需要消耗时间的操作:用户输入,读取文件。
线程的中断
一个线程是一个独立的执行路径,他是否应该结束,应该由其自身决定。
曾经使用stop方法停止但是很危险,现在一般采用给线程打标记的方法。可以用try/catch来接收线程标记的异常,再由程序员决定是否要停止。
这里Interrupted异常是中断标记,在主线程中调用interrupt方法可以启用中断标记。
在中断标记触发进入catch异常时,可以由程序员决定是否死亡。可以调用return结束线程。
守护线程
之前有提到线程分为守护线程和用户线程。
用户线程: 当一个进程不包含任何的存活的用户线程时,进程结束。
守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
方法很简单,对需要设置为守护线程的线程t1调用方法setDeamon(true)即可。
线程安全问题
用卖票实例进行说明:
模拟同时有三个线程在售票:
最后出现了票数为负数的情况:
线程安全1:同步代码块
同步代码块:
线程同步,让线程排队执行。使用加锁机制。
格式: synchronized(锁对象){} 。java中任何对象都可以作为锁存在。
将锁放入run方法里就无法排队了。这里是将锁放在if语句外。
线程安全2:同步方法
同步方法:
将判断的售票的if写一个单独的方法。在重写的run中调用。方法返回boolean值,true说明售票成功,false说明售票售空了。
如图,给方法加上synchronized
线程安全3:显式锁
显式锁:
同步代码块和同步方法都是隐式锁。开始前lock,开始后unlock。作为面向对象的语言,显式锁更能体现这一思想。
公平锁和非公平锁:
之前三种介绍的锁都是非公平锁,即锁解开后,任何线程都可以抢夺时间偏。公平锁是先来先到进行排队,在锁解开后依次执行。
在显式锁中可以头听过传递参数true让锁实现公平锁。
线程死锁
两个线程互相再等对方线程,从而一直卡住。
例如在两个试衣间中A再等B开门,B在等A开门,两个人都无法等到对方开门。
这里用了一个警察劫匪人质的实例进行说明。
public class Demo11 {
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("警察救了人质,但是罪犯跑了");
}
}
}
输出结果
这两个线程互相在等待对方线程调用方法。
在以后的开发中,在同步方法里尽量不要调用任何其他方法。
多线程通信
在Object类中有方法notify(),wait()。
在调用wait方法时,该对象线程会进入等待/睡眠。只有当时间到达或者该对象被调用时(notify)才会醒来。
经典案例:生产者与消费者问题
确保生产者生产时消费者睡眠,消费者消费时生产者睡眠。可以保障线程的安全。
这样的情况使用锁也没法很好解决,因为锁并非公平锁,会导致厨师刚做完饭又再次抢到时间偏再次做饭。
解决方案是设置休眠。如下的代码所示,创建一个flag,true进厨师false进服务生。出的时候设置为!flag,同时在厨师和服务生各自的线程运行完一次循环后唤醒进程中所有的线程,再将自己设置休眠。这样一来就实现了轮流休眠的操作。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo4 {
/**
* 多线程通信问题, 生产者与消费者问题
* @param args
*/
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.setNameAndSaste("老干妈小米粥","香辣味");
}else{
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
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 表示可以生产
private boolean flag = true;
public synchronized void setNameAndSaste(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();
}
}
}
}
}
如果没有处理休眠,输出结果出现错乱
线程的六种状态
六种状态的执行情况:
带返回值的线程Callable
对比:
格式:
一些FutureTask接口的方法。
实例:
线程池概述
创建线程 》创建任务 》执行任务 》关闭线程
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
优点:
降低资源消耗。
提高响应速度。
提高线程的可管理性。
不定长的线程池可以在一定情况下进行扩容和缓存释放。
四种线程池:
1:缓存线程池:
/**
* 缓存线程池.
* (长度无限制)
* 执行流程:
* 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());
}
});
2:定长线程池:
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
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());
}
});
3:单线程线程池:
效果与定长线程池创建时传入数值1 效果一致.
/**
* 单线程线程池.
* 执行流程:
* 1. 判断线程池的那个线 是否空闲
* 2. 空闲则使用
* 4. 不空闲,则等待池中的单个线程空闲后使用
*/
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());
}
});
4:周期性任务定长线程池:
public static void main(String[] args) {
/**
* 周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
}
线程池实例:
缓存线程池:线程池的对象都是ExecutorService。
创建缓存线程池是Executors.newCachedThreadPool()。
随后加入一个休眠。
程序执行时会等三个线程都结束后主线程再次使用线程池中的缓存线程。
定长线程池:
这里给定线程池长度为2,即同时有两个线程在执行,写三个输出打印的话会先输出两个锄禾日当午,在三秒后线程1再被使用输出第三次锄禾日当午。0
单线程线程池:
池里只有一个线程:
输出之后只有一个线程在执行。
P.S: 在线程池执行之后代码并不会第一时间关闭,可能在等待后续的任务传入。
周期定长线程池:
流程和定长线程池一样,在调用其方法schedule()时需要传三个参数。
定时执行:
在五秒之后,开始执行输出。
周期执行:需要传四个参数。