1.线程与进程
1.1概念
-
进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
-
线程
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少
有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分
成若干个线程
-
线程阻塞
休眠sleep()方法;或执行某行读取文件的代码;或执行等待输入的代码。
又称耗时操作,总之停在那里就算
1.2分时调度
-
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
-
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为抢占式调度。
-
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
2.同步与异步&并行与并发
-
同步:排队执行 , 效率低但是安全.
-
异步:同时执行 , 效率高但是数据不安全.
-
并发:指两个或多个事件在同一个时间段内发生。
-
并行:指两个或多个事件在同一时刻发生(同时发生)。
3.Thread
多线程常用实现方法之一:继承Thread类
public static void main(String[] args){
MyThread m = new MyThread();
m.start();
for(int i=0;i<10;i++){
System.out.println("线程二执行"+i);
}
}
public class MyThread extends Thread{
//run()方法就是线程要执行的任务,但该线程的触发方式,不是调用run(),而是通过Thread对象的start()方法来启动任务
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("线程一执行"+i);
}
}
}
4.Runnable
多线程常用实现方法之二:实现Runnable接口
public static void main(String[] args){
//1.创建一个任务对象
MyRunnable r = new MyRunnable();
//2.创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
//3.执行这个线程
t.start();
//也可简化成new Thread(new MyRunnable()).start();
//main中的线程二,用于作对比体现线程同时执行
for(int i=0;i<10;i++){
System.out.println("线程二执行"+i);
}
}
//用于给线程执行的任务
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("线程一执行"+i);
}
}
}
Runnable相比Thread的优点:
a.通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况,更灵活;
b.可避免单继承带来的局限性,java只可以单继承,但可以多实现;
c.任务与线程本身是分离的,提高了程序的健壮性;
d.后续学习的线程池技术,接受Runnable类型的任务单不接收Thread类型的线程。
那就不用Thread了吗?也不全是,它有一种简便的实现线程方法:
public static void main(String[] args){
new Thread(){//用的匿名内部类
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("线程一执行"+i);
}
}
}.start();
for(int i=0;i<10;i++){
System.out.println("线程二执行"+i);
}
}
5.Thread类
Thread类常用方法:
1.Thread构造方法:
Thread(Runnable target);
//可定义线程的名字,还有get方法可获取名字
Thread(Runnable target,String name);
Thread(String name);
2.setPriority()//设置线程的优先级
3.sleep(long millis)//使当前正执行的线程休眠(比如想隔多长时间输出一下...)
如sleep(1000)——每隔一秒
4.setDaemon(boolean on)//将此线程标记为Daemon(守护)线程或用户线程
//守护线程是依附与用户线程的,用户线程没了它也没了
5.设置和获取线程名称
System.out.println(Thread.currentThread().getName());
//currentThread()方法可获取当前线程
//getName()方法可获取线程名称
6.线程中断
一个线程是一个独立的执行路径,它是否应该结束,应该由其本身决定。
也就是说不应该由外部强行中断,否则会导致该部分内存无法释放。
7.守护线程
线程分为用户线程和守护线程。
用户线程:
守护线程:
8.线程安全
经典案例:多线程抢票问题
public static void main(String[] args){
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//票数
private int count = 10;
@Override
public void run(){
while(count>0){
//卖票
System.out.println("正在准备卖票");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票:"+count);
}
}
}
该代码有可能出现不合理情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YGfCc6h-1618823572231)(C:\Users\jy\AppData\Roaming\Typora\typora-user-images\image-20210417172546374.png)]
发生原因:main方法中同时执行了三条线程,有可能当count=1的时候,某一线程抢到了,进入run方法的循环,但还未执行到count–的时候,有其他线程抢到了时间片,也进入了循环,则可能有多个线程同时进入到循环,最后余票减到负数。
我们加了个sleep()是为了让这种异常出现的可能性增大以示说明。
9.线程安全处理方法
9.1方法一:同步代码块。
给run方法用synchronized上锁,使一个线程在运行时不会有其他线程插入。此方法效率会降低。以下是上述问题案例的解决代码:
public static void main(String[] args){
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//票数
private int count = 10;
Object o = new Object();
@Override
public void run(){
//Object o = new Object();
//此处是一个误区,如果new对象o放在run里面,则线程不安全问题解决不了,因为每个线程执行的时候都自己创一把锁,不是同一把锁就无法排队。
while(true){
synchronized(o){
//三个线程必须同时在执行一个任务,故前面new了个Object o
if(count>0){
//卖票
System.out.println("正在准备卖票");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}else{
break;//count=0时要结束
}
count--;
System.out.println("出票成功,余票:"+count);
}
}
}
}
}
9.2方法二:同步方法。
将同步方法写成方法。
public static void main(String[] args){
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//票数
private int count = 10;
@Override
public void run(){
whiel(true){
boolean flag = sale();
if(!flag){
break;
}
}
}
//将同步部分写成方法
public synchronized boolean sale(){
//加上synchronized修饰符就可以实现排队了
if(count>0){
//卖票
System.out.println("正在准备卖票");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}else{
break;//count=0时要结束
}
count--;
System.out.println("出票成功,余票:"+count);
return true;
}
return false;
}
这种将同步操作写成方法,锁是谁?
是调用此方法的this对象
验证方法:main部分原来只new了一个对象,现改成new三个对象
public static void main(String[] args){
//Runnable run = new Ticket();
//new Thread(run).start();
//new Thread(run).start();
//new Thread(run).start();
new Thread(new Ticket()).start();
new Thread(new Ticket()).start();
new Thread(new Ticket()).start();
}
则每次抢票会同时弹出三个打印,线程间不是排队的,因为是new Ticket()在调用,此处是三个Ticket对象分别调用,则没排队。得是同一个对象,运行三个线程才可排队。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M8uXXdYX-1618823572234)(C:\Users\jy\AppData\Roaming\Typora\typora-user-images\image-20210417212719988.png)]
9.3显式锁lock
前述两种方法是隐式锁,这种方法是自己创建一把锁,手动上锁解锁。
public static void main(String[] args){
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//票数
private int count = 10;
//显式锁
private Lock l = new ReentrantLock();
@Override
public void run(){
while(true){
l.lock();//开始卖票前手动上锁
if(count>0){
//卖票
System.out.println("正在准备卖票");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票:"+count);
}else{
break;
}
l.unlock();//结束后手动解锁
}
}
}
10.公平锁与非公平锁
公平锁是先来先到,解锁后按到来的顺序执行,也就是有在等解锁的时候有排队的过程。
非公平锁就是解锁后大家抢。
上述三种方法默认是非公平锁
实现公平锁:新建显式锁时传入参数true。
private Lock l = new ReentrantLock(true);
11.线程死锁
形象描述:警察说你放了人质我放了你。罪犯说你放了我我放了人质,两个。陷入死循环。
避免方法:在所有有可能导致锁产生的方法里,不用再调用另外一个方法,导致另外的锁产生。
示例:
12.多线程通信问题
生产者与消费者问题
package com.java.demo;
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();
}
}
}
}
}
执行结果:厨师生产一份服务员端走一份,一份煎饼果子一份老干妈,且味道不对应错。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMhTpW9c-1618823572236)(C:\Users\jy\AppData\Roaming\Typora\typora-user-images\image-20210417234142150.png)]
13.线程的六种状态
new(创建了但未执行)、Blocked(阻塞,也即线程排队的状态)、Runnable(执行)、Waiting、TimeWaiting(定时等待)、Terminated(退出)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5T2dOYDo-1618823572237)(C:\Users\jy\AppData\Roaming\Typora\typora-user-images\image-20210418003125746.png)]
14.Callable
比较特殊的一种线程创建方式
之前的创建方式是主线程和其他线程并行,callable则是既可以并行也可以作为主线程的一个任务,主线程等它执行完了再接着执行。
既然主线程要等它执行完,那还要多线程有何用?——如果这种callable同时有多个进行,就有用,且能体现主次关系。
非并行实现的具体方法:主线程调用Callable.get()方法获取返回值:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
new Thread(future).start();
15.线程池
不作为重点学习,知道有这么个东西就行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mr2wTnWm-1618823572241)(C:\Users\jy\AppData\Roaming\Typora\typora-user-images\image-20210419113224173.png)]
15.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());
}
});
15.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());
}
});
15.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());
}
});
15.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);
}
16.Lambda表达式
是函数式编程思想,不关注过程只注重结果,我实现功能只是需要方法即可,有时候没必要创建个对象再调用方法。与面向对象思想相对。
在多线程这块这个方法的简化功能能够特别体现。
public static void main(String[] args){
//1.面向对象思想方法:匿名内部类(匿名内部类都已经是简化过了的),还是较为冗余
Thread t = new Thread(new Runnable(){
@Override
public void run(){
System.out.println("锄禾日当午");
}
});
t.start();
//2.用Lambda表达式,函数式编程思想
Thread t = new Thread(() -> {
System.out.println("锄禾日当午");
});
t.start();
//效果是完全一样的,等于省掉了创建对象的过程
}
ad(future).start();
### 15.线程池
不作为重点学习,知道有这么个东西就行。
[外链图片转存中...(img-mr2wTnWm-1618823572241)]
15.1缓存线程池
```java
/**
\* 缓存线程池.
\* (长度无限制)
\* 执行流程:
\* 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());
}
});
15.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());
}
});
15.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());
}
});
15.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);
}
16.Lambda表达式
是函数式编程思想,不关注过程只注重结果,我实现功能只是需要方法即可,有时候没必要创建个对象再调用方法。与面向对象思想相对。
在多线程这块这个方法的简化功能能够特别体现。
public static void main(String[] args){
//1.面向对象思想方法:匿名内部类(匿名内部类都已经是简化过了的),还是较为冗余
Thread t = new Thread(new Runnable(){
@Override
public void run(){
System.out.println("锄禾日当午");
}
});
t.start();
//2.用Lambda表达式,函数式编程思想
Thread t = new Thread(() -> {
System.out.println("锄禾日当午");
});
t.start();
//效果是完全一样的,等于省掉了创建对象的过程
}