第四章第五节Java核心类库_多线程
多线程
一.线程与进程
1.线程与进程
程序(program):
是为了完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。(程序是静态的)
进程(process):
是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。
进程是动态的,是一个动的过程,有它自身的产生、存在和消亡的过程。
线程(thread):
进程可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程。
2.线程调度
目的是为了更合理的利用CPU
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度(Java使用的调度方式):
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
但其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
二.同步与异步&并发与并行
1. 同步与异步
同步: 排队执行 ,效率低但是安全
异步: 同时执行 ,效率高但是数据不安全
2. 并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
三.继承Thread
1.代码块
Demo1
public class Demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for(int i = 0; i < 10; i++){
System.out.println("我要睡觉!"+i);
}
}
MyThread
public class MyThread extends Thread{
/**
* run方法就是线程要执行的任务方法
*/
@Override
public void run() {
//这里的代码就是一条新的执行路径
//这个执行路径的触发方式不是调用run方法,
//而是通过thread对象的start()来启动任务
for(int i = 0; i < 10; i++){
System.out.println("我要吃饭!"+i);
}
}
}
2.运行结果
3.时序图
补充:每个线程都有自己的栈空间,共用一份堆内存。
四.实现Runnable(推荐使用)
1.使用方法
另一种实现多线程的方法之步骤:
①创建自定义类implements实现Runnable接口,并重写run方法;
②用自定义类创建一个对象r;
③用Thread类创建一个对象t,并将r作为t构造方法的参数;
2.代码块
实现Runnable与继承Thread相比有如下优势
1,通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况;
2,可以避免单继承所带来的局限性(Java允许实现多个接口,但不允许继承多个父类);
3,任务与线程是分离的,提高了程序的健壮性;
4,后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程;
Demo1
public class Demo1 {
public static void main(String[] args) {
//实现Runnable
//1. 创建一个任务对象
MyRunnable r = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
//3. 执行这个线程
t.start();
for(int i = 0; i < 10; i++){
System.out.println("我要睡觉!"+i);
}
}
}
MyRunnable
/**
* 用于给线程执行的任务
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
//线程的任务
for (int i=0;i<10;i++){
System.out.println("我要上厕所"+i);
}
}
}
3.thread也有一定的好处
public class Demo1 {
public static void main(String[] args) {
//若只调用一次,用thread可以通过匿名内部类的方式,使代码更加简洁
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);
}
}
}
五.Thread类
1.常用构造方法
2.其他常用方法
用于设置优先级的字段
六.设置和获取线程名称
MyRunnable
/**
* 用于给线程执行的任务
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
//线程的任务
//静态方法currentThread可以获得当前线程,调用getName/setName
// 可以获得/设置线程的名称
System.out.println(Thread.currentThread().getName());
}
}
Demo1
public class Demo1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
new Thread(new MyRunnable(),"喜羊羊").start();
new Thread(new MyRunnable(),"美羊羊").start();
new Thread(new MyRunnable(),"懒羊羊").start();
}
}
运行结果
七.线程休眠sleep
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++){
System.out.println(i);
Thread.sleep(1000);
}
}
}
八.线程的中断
过时的stop方法可以直接中断线程,但是如果线程来不及释放资源,会造成一部分垃圾无法回收;
这里采用添加中断标记的方法:调用interrupt方法,子线程执行时捕获中断异常,并在catch块中,添加处理释放资源的代码。
1.代码
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//线程的中断
//一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定
Thread t = new Thread(new MyRunnable(), "子线程");
t.start();
for (int i =0;i < 5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000);
}
t.interrupt();//主线程for循环执行完毕后,将子线程中断
}
static class MyRunnable implements Runnable {
@Override
//这里的run方法不能将异常抛出,因为这里父接口Runnable没有声明异常的抛出
//子接口不能声明比父接口范围更大的异常
//所以只能进行try-catch
public void run() {
for (int i =1;i <= 10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("我挂机了嘤嘤嘤~");
return;
}
}
}
}
}
2.运行结果
九.守护线程
概述:
线程分为守护线程和用户线程;
- 用户线程:当一个进程不包含任何存活的用户线程时,进程结束;
- 守护线程:守护用户线程,当最后一个用户线程结束后,所有守护线程自动死亡;
直接创建的都是用户线程;
设置守护线程:线程对象.setDaemon(true),而且一定要在启动之前设置。
十.线程安全
线程不安全的原因:
多个线程争抢同一个数据,使得数据在判断和使用时出现不一致的情况。
解决方案1-同步代码块
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
//线程不安全
//解决方案1 同步代码块
//格式:synchronized(锁对象){}
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 Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}//synchronized结束
}//while结束
}//run结束
}//Ticket结束
}//Demo1结束
解决方案2-同步方法
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
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 (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
给方法上锁,对应的锁对象就是this, 如果是静态修饰方法的话,锁对象为类名.class;
比如这里sale方法若被修饰为静态方法的话,锁对象为Ticket.class,也就是字节码文件对象。
解决方案3-显式锁Lock
同步方法和同步代码块都属于隐式锁,显式锁则是程序员手动加锁、解锁;
public class Demo{
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案3 显示锁 Lock 子类 ReentrantLock
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;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@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(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
//把锁打开!
l.unlock();
}
}
}
}
十一.公平锁与非公平锁
-区别
公平锁:先来先得,遵循排队;
非公平锁:大家一起抢(同步代码块,同步方法,显式锁都属于非公平锁);
-实现方法
在显式锁实例化时,传入参数true()
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
十二.死锁典例
public class Demo {
/**
* 死锁典例:两个线程互相申请对方的锁,但是对方都不释放锁。
* @param args
*/
public static void main(String[] args) {
//线程死锁
Culprit culprit = new Culprit();
Police police = new Police();
//新建一个线程,警察对罪犯说话,等待罪犯回应
//警察的say方法里面打印完自己说的话之后,便等待罪犯的回应
//而另一边罪犯的say方法也在等待警察的回应,且罪犯的say是带锁的
//故罪犯的reaction方法一直到不了,警察的say就一直在等罪犯的reaction
new MyThread(culprit,police).start();
//main主线程,罪犯对警察说话,等待警察回应
//罪犯的say方法里面打印完自己说的话之后,便等待警察的回应
//而另一边警察的say方法也在等待罪犯的回应,且警察的say是带锁的
//故警察的reaction方法一直到不了,罪犯的say就一直在等警察的reaction
culprit.say(police);
}
static class MyThread extends Thread{
//私有两个属性,一个警察p一个罪犯c
private Culprit c;
private Police p;
//两参的构造方法
public 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.reaction();
}
public synchronized void reaction(){
System.out.println("罪犯说:好的,我放了人质,你放过我");
}
}
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察说:你放了人质,我放过你");
c.reaction();
}
public synchronized void reaction(){
System.out.println("警察说:好的,我放了你,你把人质给我");
}
}
}
解决方案:
在任何有可能导致锁产生的方法里,不要再调用另外一个方法让另外一个锁产生;
一个方法已经产生一个锁了,就不要再去找其他有可能产生锁的方法了。
十三.多线程通信问题
主要借助于wait和notify函数实现
十四.生产者与消费者
思路:
厨师cook为生产者线程,服务员waiter为消费者线程,食物为生产与消费的物品;
假设目前只有一个厨师,一个服务员,一个盘子。理想状态是:厨师生产一份饭菜,服务员端走一份,且饭菜的属性未发生错乱;
厨师可以制作两种口味的饭菜,制作100次;
服务员可以端走饭菜100次。
Test one:
public class TestOne{
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("煎饼果子","甜辣味");
}//else结束
}//for结束
}//run结束
}//Cook结束
//服务员
static class Waiter extends Thread{
private Food f;
//构造方法
public Waiter(Food f) {
this.f = f;
}
//上100次菜
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);//端完一次休息一下:因为厨师在setNameAndTaste中有休眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();//端菜
}//for结束
}//run结束
}//Waiter结束
//食物
static class Food{
private String name;
private String taste;
// 生产
public void setNameAndTaste(String name,String taste){
this.name = name;
//设置名称和味道之间加入休眠,为了演示:在此期间时间片可能发生丢失,因而菜色属性发生错乱的情况。
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
// 消费
public void get(){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
}//get结束
}//Food结束
}//TestOne结束
运行结果
原因分析:在厨师进行setNameAndTaste的时候,先设置了name,此时休眠了0.1秒,而taste还未来得及切换,服务员此时就把菜端出去了,出现了多线程协同合作的不匹配问题。
Test two:
给get、setNameAndTaste两个方法都加上同步标记synchronized,让厨师在进行setNameAndTaste时候服务员别进来瞎掺和进行get,服务员get的时候厨师别进来瞎掺和进行setNameAndTaste。
错误原因分析:synchronized只是确保了方法内部不会发生线程切换,但并不能保证生产一个消费一个的逻辑关系。
Test three:
终极解决方案:
厨师做完饭后喊醒服务员,自己睡着;服务员送完饭后喊醒厨师,自己睡着。主要修改的部分为setNameAndTaste与get方法。
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;
//做完了把标记改为false
flag = false;
//唤醒在当前this对象下所有睡着的线程(即唤醒服务员)
this.notifyAll();
//厨师自己睡过去
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}//setNameAndTaste结束
// 消费
public synchronized void get(){
if (!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
//端完了把标记改为true
flag = true;
//唤醒在当前this对象下所有睡着的线程(即唤醒厨师)
this.notifyAll();
//服务员自己睡过去
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}//get结束
运行结果
十五.线程的六种状态
十六.线程创建的第三种方式-带返回值的线程Callable
新的创建线程的方式。之前的创建线程方式:实现Thread的子类、实现Runnable接口,可以看成是和主线程并发执行的;这里要讲的线程更像是主线程指派的一个任务,主线程可以获得其返回值。
后续开发使用较少,仅作了解,不再赘述。
十七.线程池 Executors
为什么需要线程池?
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
后续开发中使用到线程池的地方很少,不是重点,不再赘述,详情看老师发的pdf。
十八.Lambda表达式
为什么要用lambda表达式?
答: 对于某些应用场景,我们更注重于结果,如果能用一个方法解决, 那么通过创建对象、调用方法的方式可能会更加繁琐。
1)不使用lambda
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
print(new MyMath() {
@Override
public int sum(int x, int y) {
return x + y;
}
}, 100, 200);
}
public static void print(MyMath m, int x, int y){
int num = m.sum(x, y);
System.out.println(num);
}
static interface MyMath{
int sum(int x, int y);
}
}
2)使用lambda
不需要实现接口、实例化对象;
public class Demo1 {
/**
* lambda表达式
* 函数式编程思想(注重结果,而面向对象则是通过创建对象,解决问题)
* @param args
*/
public static void main(String[] args) {
print((int x, int y) -> {
return x + y;
}, 100, 200);
}
public static void print(MyMath m, int x, int y){
int num = m.sum(x, y);
System.out.println(num);
}
static interface MyMath{
int sum(int x, int y);
}
}
参考链接:https://blog.csdn.net/qq_41528502/article/details/108052873
转载:https://blog.csdn.net/tangyuan__/article/details/115495219