天天看点

【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

文章目录

  • 多线程
    • 一、线程与进程
    • 二、线程调度
    • 三、同步与异步&并发与并行
    • 四、多线程实现方式
      • 1、继承Thread
      • 2、实现Runnable
      • 3、实现Runnable和继承Thread比较
      • 4、匿名内部类实现方式
    • 五、Thread类
    • 六、设置和获取线程名称
    • 七、线程休眠sleep
    • 八、线程的中断
    • 九、守护线程
    • 十、线程安全问题
      • 1、同步代码块
      • 2、同步方法
      • 3、显式锁Lock
        • 显式锁和隐式锁的区别
      • 4、线程死锁
        • 1.多线程通信问题
    • 十一、线程的六种状态
    • 十二、带返回值的线程Callable
    • 十三、线程池
      • 1、缓存线程池
      • 2、定长线程池
      • 3、单线程线程池
      • 4、周期性任务定长线程池
    • 十四、Lambda表达式
  • 总结

多线程

多线程:栈空间独立,堆内存共享。

一、线程与进程

  • 进程

正在运行的应用程序:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间

每个进程都有着自己独立的堆、栈且是互不共享的

  • 线程
    • 进程中的一个执行路径(一个应用程序从执行到结束的整个过程),共享一个内存空间,一个进程中可以包含多条线程;
    • 线程之间可以自由切换,并发执行;
    • 一个进程最少有一个线程,线程控制着进程。

二、线程调度

  1. 分时调度

    所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。

  2. 抢占式调度

    优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java使用的为 抢占式调度。

  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

三、同步与异步&并发与并行

同步:排队执行 , 效率低但是安全

异步:同时执行 , 效率高但是数据不安全

并发:指两个任务在同一时间段内都请求运行

处理器只能接收一个任务,把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行

  • 如果用一台电脑先给友人A发个消息,然后立刻再给友人B发消息,然后再跟友人A聊,再跟友人B聊。这就叫并发。

并行:两个任务同一时刻运行,需要多核CPU

  • 比如跟两个朋友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。

四、多线程实现方式

1、继承Thread

步骤:

  1. 定义MyThread类并继承Thread类;
  2. 重写run方法,新的执行路径(通过thread对象的start()启动任务)
/**
 * @author Elvira
 * @date 2020/10/12 16:01
 * @description 继承Thread
 */
public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("嘿……"+i);
        }
    }
}
           
  1. 创建子类对象
  2. 调用start(),让线程执行
/**
 * @author Elvira
 * @date 2020/10/12 16:08
 * @description 继承Thread的程序入口
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyThread m = new MyThread();
        m.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("嘻嘻……"
            +i);
        }
    }
}
           
  • 运行结果:可以看到顺序并不统一,这是因为线程在抢占时间片,谁先抢到就是谁的
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

线程时序图:

【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

注意:运行过程中子线程任务中调用的方法都在子线程中运行

2、实现Runnable

步骤:

  1. 实现Runnable接口;
  2. 实现抽象方法run(),编写线程的任务;
/**
 * @author Elvira
 * @date 2020/10/12 16:22
 * @description 实现Runnable
 */
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("哼哼!" + i);
        }
     }
}
           
  1. 创建一个任务对象
  2. 创建一个线程,并给它一个任务
  3. 调用start()方法执行线程
/**
 * @author Elvira
 * @date 2020/10/12 16:30
 * @description 实现Runnable的程序入口
 */
public class RunnableTest {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("呵呵?" + i);
        }
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

3、实现Runnable和继承Thread比较

继承Thread

  • 优点:直接使用Thread类中的方法,代码简单
  • 弊端:如果已有父类,不可用

实现Runnable接口(更常用)

  • 其优点:

    ​ 1. 通过创建任务,给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况

    1. 可以避免单继承带来的局限性
    2. 任务和线程分离,提高程序健壮性
    3. (最重要)后续提供的线程池任务,接收Runnable类型任务,不接收Thread类型线程
  • 弊端:不能直接使用Thread中的方法,需先获取线程对象,再得到Thread的方法,代码复杂

4、匿名内部类实现方式

  1. 继承Thread
/**
 * @author Elvira
 * @date 2020/10/12 16:52
 * @description 继承thread的匿名内部类实现方式
 */
public class MyThread0 {
    public static void main(String[] args) {
        new Thread() {
            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);
        }
    }
}
           
  • 运行结果:
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
  1. 实现Runnable
new Thread(new Runnable() {				
    public void run() {			   
        for(int i = 0; i < 5; i++) {	
            System.out.println("aaa" + i);
        }
    }
}).start(); 
for (int i = 0; i < 5; i++) {
    System.out.println("bbb" + i);
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

五、Thread类

接下来列举API中Thread类最常用的方法:

构造器 描述

Thread()

分配新的

Thread

对象。

Thread(Runnable target)

分配新的

Thread

对象。

Thread(Runnable target, String name)

分配新的

Thread

对象。

Thread(String name)

分配新的

Thread

对象。

常见方法:

变量和类型 方法 描述

long

getId()

返回此Thread的标识符。

String

getName()

返回此线程的名称。

int

getPriority()

返回此线程的优先级。

void

setPriority(int newPriority)

更改此线程的优先级。

void

start()

导致此线程开始执行; Java虚拟机调用此线程的

run

方法。

static void

sleep(long millis)

导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。

static void

sleep(long millis, int nanos)

导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。

void

setDaemon(boolean on)

将此线程标记为 daemon线程或用户线程。

特殊字段:控制线程抢到时间片的几率

变量和类型 字段 描述

static int

MAX_PRIORITY

线程可以拥有的最大优先级。

static int

MIN_PRIORITY

线程可以拥有的最低优先级。

static int

NORM_PRIORITY

分配给线程的默认优先级。

六、设置和获取线程名称

首先需要了解:currentThread() 可以获取当前正在执行的线程对象

  1. 获取线程名称
/**
 * @author Elvira
 * @date 2020/10/12 17:41
 * @description 获取线程名称
 */
public class NameTest {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        //给线程指定一个名称
        new Thread(new MyRunnable(), "Elvira").start();
    }
}
           

实现类:

public class MyRunnable implements Runnable {   
    @Override
    public void run() {
 		System.out.println(Thread.currentThread().getName());
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

如果不指定线程名称,直接给线程一个任务:再加两句

new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
  1. 设置线程名称

方式一:上述,直接在创建线程任务时指定名称

//给线程指定一个名称
        new Thread(new MyRunnable(), "Elvira").start();
           

方式二:setName()设置

Thread t = new Thread(new MyRunnable());
t.setName("THREAD");
t.start();
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

七、线程休眠sleep

sleep是Thread类的静态方法,类名直接调用即可。单位ms。

/**
 * @author Elvira
 * @date 2020/10/12 20:13
 * @description 线程休眠3秒
 */
public class SleepTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            Thread.sleep(3000);
        }
    }
}
           
  • 运行结果:

    每隔3秒蹦出来一个数字,最终打印了10个数字

线程阻塞:所有较耗时的操作都能称为阻塞。也叫耗时操作。

八、线程的中断

一个线程是一个独立的执行路径,它是否应该结束,由其自身决定。

因为线程执行过程会有很多资源需要使用或释放,如果干涉它的结束,很可能导致资源没能来得及释放,一直占用产生无法回收的内存垃圾。

早期有stop()方法可以结束线程,现在已经过时。现在出了新的方法,给线程打标记来控制它的结束。

标记是什么:interrupt

示例:

第一步:先设置main线程和Thread-0线程都执行循环数字1-10的任务

/**
 * @author Elvira
 * @date 2020/10/12 20:28
 * @description 线程中断
 */
public class InterruptTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           

实现类:

public class  MyRunnable implements Runnable{    
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

第二步:给线程t1添加中断标记interrupt()

/**
 * @author Elvira
 * @date 2020/10/12 20:28
 * @description
 */
public class InterruptTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        t1.interrupt();
    }
}
           

实现类:

public class  MyRunnable implements Runnable{    
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //e.printStackTrace();
                System.out.println("发现了中断标记,还没有停止。");
            }
        }
    }
}
           
  • 运行结果:
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

解释:发现了中断就会捕捉异常进入catch块,程序员决定该如何处理。不进行任何处理程序还会继续执行。此时在catch块中加入return,程序就会中断。

public class  MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //e.printStackTrace();
                System.out.println("发现了中断标记,在此处停止。");
                return;
            }
        }
    }
}
           
  • 运行结果:线程结束(”自杀“)
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

九、守护线程

线程分为守护线程和用户线程

  • 用户线程:当一个进程不包含任何存活的用户线程时,进行结束。
  • 守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。

设置线程为守护线程:启动之前设置

完整代码:

/**
 * @author Elvira
 * @date 2020/10/13 9:32
 * @description
 */
public class SetDaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.setDaemon(true);
        t1.start();

        for (int i = 0; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
           

实现类:

public class MyRunnable implements Runnable{    
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
        }
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

十、线程安全问题

举一个生活中的小栗子,模拟买票窗口,创建三个线程,一起卖10张票。此时就会出现线程不安全的问题。

/**
 * @author Elvira
 * @date 2020/10/13 9:49
 * @description
 */
public class ThreadSafeTest {
    public static void main(String[] args) {
        Runnable run = new Ticket();
        //创建三个线程
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

}
           

买票任务:

/**
* 模拟买票窗口
*/
public 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);
        }
    }
}
           
  • 运行结果
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

结果:发现票数出现负数,而我们设定只有票数大于0(count>0)时才会进入循环。

原因:线程争抢,导致线程不安全。 多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂买票,最终导致卖的票出现负数。

线程不安全的原因
  • ★当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
    • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

如何保证线程安全性?

让它们排队执行,一个一个来就安全了。这个排队方式就是给线程加锁。

1、同步代码块

  • 使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
  • 多个同步代码块如果使用相同的锁对象, 那么他们就是同步的

语法格式:

synchronized(锁对象) {}

任何对象都可以作为锁存在。可以一行代码加锁。

例:

/**
 * 模拟卖票窗口
 */
public class Ticket implements Runnable {
    private int count = 10;//票数
    private Object o = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (o) {
                if (count > 0) {
                    //开始卖票
                    System.out.println("正在卖票");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
                } else {
                    return;
                }
            }
        }
    }
}
           
  • 运行结果:
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

2、同步方法

以方法为单位进行加锁。把synchronized关键字修饰在方法中。

例:此时写一个方法sale()执行卖票任务,不给方法加锁

/**
 * @author Elvira
 * @date 2020/10/13 14:22
 * @description 同步方法
 */
public class Ticket implements Runnable {
        private int count = 10;
        @Override
        public void run() {
            while (true) {
                boolean flag = sale();
                if (!flag) {
                    break;
                }
            }
        }

        public 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;
        }
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

这时线程仍然不安全,我们给方法加上锁(synchronized关键字):

public synchronized boolean sale() {
    //卖票逻辑
}
           

必须是同一个Runnable对象。

  • 此时运行结果:安全了
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

3、显式锁Lock

Lock l = new ReentrantLock():自己创建一把锁

lock():加锁

unlock():解锁

例:

/**
 * @author Elvira
 * @date 2020/10/13 14:46
 * @description 显式锁Lock
 */
public 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(Thread.currentThread().getName() + "出票成功,余票:" + count);
                } else {
                    break;
                }
                l.unlock();
            }
        }
    }
}
           

显式锁和隐式锁的区别

隐式锁:同步代码块和同步方法的synchronized

显式锁:Lock

热心网友详解: Java并发之显式锁和隐式锁的区别。

区别 synchronized lock
原始构成 Java关键字,由JVM维护,是JVM层面的锁 JDK1.5之后的类,使用lock是在调用API,是API层面的锁
使用方式 隐式锁,不需要手动获取和释放锁,只需要写synchronized,不用进行其他操作 显式锁,需要手动获取和释放锁,如果没有释放锁,可能会出现死锁
等待中断 不会中断,除非抛出异常或正常运行完成 可以中断,1:调用设置超时方法tryLock(long timeout ,timeUnit unit);2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
加锁公平 非公平锁 可以是公平锁也可以是非公平锁,默认是非公平锁。可以在其构造方法传入Boolean值,true公平锁,false非公平锁
绑定多个条件 没有。不能精确唤醒线程,要么随机唤醒一个线程,要么唤醒所有等待线程 用来实现分组唤醒需要唤醒的线程,可以精确唤醒线程
性能 JDK1.5时性能较低,JDK1.6时性能优化,与lock相较无异 JDK1.5时性能更高,JDK1.6时synchronized优化赶上lock
加锁方式 线程获取独占锁(CPU悲观锁机制),只能依靠阻塞等待线程释放锁。在CPU转换线程阻塞时会引起线程上下文切换,当竞争锁的线程过多时,会引起CPU频繁上下文切换导致效率低下 使用乐观锁机制(CAS操作 Computer and Swap),假设不会发生冲突,一旦发生冲突失败就重试,直到成功为止。
  • 公平锁:先来先到
  • 非公平锁:大家一起抢,谁抢着是谁的

4、线程死锁

多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁。

当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。

一个线程可以拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

例:举一个生活中的小栗子:警察和绑架犯

  • Culprit:罪犯类
  • Police:警察类
  • MyThread:实现Runnable接口的实现类
  • WaitTest:程序入口,start线程

罪犯类:

/**
 * 罪犯
 */
public class Culprit {
    public synchronized void say(Police p) {
        System.out.println("罪犯:你放过我,我放了人质。");
        p.fun();
    }
    public synchronized void fun() {
        System.out.println("罪犯被放过了,罪犯也放了人质。");
    }
}
           

警察类:

/**
 * 警察
 */
public class Police {
    public synchronized void say(Culprit c) {
        System.out.println("警察:你放了人质,我放过你。");
        c.fun();
    }
    public synchronized void fun() {
        System.out.println("警察救了人质,但罪犯跑了。");
    }
}
           

Runnable实现类:写一个能传Culprit和Police对象的构造方法。子线程给p对象的say方法传入对象c,让警察说话,罪犯回应

static class MyThread extends Thread {
    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);
    }
}
           

主线程:启动子线程,给c对象的say方法传入p,让罪犯说话,警察回应。

public static void main(String[] args) {
    Culprit c = new Culprit();
    Police p = new Police();

    new Thread(new MyThread(c, p)).start();
    c.say(p);
}
           
  • 运行结果:程序卡在这不动
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

注意:在开发过程中要尽量避免死锁的发生

如何避免死锁?

在根源解决:在任何可能导致锁产生的方法中,不要调用另一个也可能产生锁的方法。

上面的例子修改一下:在调用罪犯的say方法前,还没启动子线程,就不会锁上警察的say方法。等待罪犯说完,警察也回应完,再启动子线程。

c.say(p);
new Thread(new MyThread(c, p)).start();
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

(题外话:听说电脑越好未阻塞(如上图片中)效果出现越慢,电脑越差出现越快。然后本人电脑一次成功,该攒钱换新电脑了。。。哭唧唧)

1.多线程通信问题

Object类中的相关方法:

变量和类型 方法 描述

void

wait()

导致当前线程等待它被唤醒,通常是 通知或 中断 。

void

wait(long timeoutMillis)

导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。

void

wait(long timeoutMillis, int nanos)

导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。

void

notify()

唤醒正在此对象监视器上等待的单个线程。

void

notifyAll()

唤醒等待此对象监视器的所有线程。

什么时候需要通信

多个线程并发执行时, 在默认情况下CPU是随机切换线程的

如果我们希望他们有规律的执行, 就可以使用通信, 例如每个线程执行一次打印

  • 等待 (拿着锁的:高风亮节,让步)

    必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。 同时此线程因obj而处在无限期等待 的状态中。释放锁,进入等待队列。

  • 通知

    必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响。

例:生产者与消费者

  • Cook类:厨师类,子线程,做饭
  • Waiter类:服务生类,子线程,端饭
  • Food类:包含菜的名称和属性,还有设置、获取菜的名称属性的方法
  • Test类:主线程,创建食物对象和启动子线程

第一步:先简单罗列

Cook类:

/**
 * 厨师
 */
public 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("杂粮奶粉","酸甜味");
            }
        }
    }
}
           

Waiter类:

/**
 * 服务生
 */
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();
        }
    }
}
           

Food类:

/**
 * 食物
 */
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 +"\t味道:" + taste);
    }
}
           

主线程:

/**
 * @author Elvira
 * @date 2020/10/13 18:46
 * @description 生产者与消费者
 */
public class Test {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
}
           
  • 运行结果:(有问题)
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结

此时出现了问题,杂粮奶粉变成了五香味,西红柿炒红薯变成了酸甜味,结果错乱了。

如何解决这个问题?

第二步:为解决线程错乱,需要进行线程之间的通信

  • 给Food类中添加一个标记,标记此时是在做饭还是在端饭。
  • 在设置菜名和味道方法中添加条件,在执行完之后设置标记为false,
    • 唤醒所有this线程中睡着的线程(端菜)
    • 再让Cook线程睡着
    if (flag == true) {
        
        this.name = name;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
        
        flag = false;
        
    }
               
  • 在get方法中,flag默认值为false,服务员可以端菜,上菜后flag又变为true,Cook又能做菜
    • 再把this中睡着的线程唤醒(厨师)
    • 再让自己(Waiter)睡着
    if (!flag) {
        System.out.println("服务员端走了:"+ name +"\t味道:" + taste);
        flag = true;
        this.notifyAll();
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
               

解释:

  • 厨师做完饭,叫醒服务生端菜,自己再睡着;
  • 服务生端完菜,叫醒厨师,自己再睡着。

交替完成,保证线程一定不会出错。

十一、线程的六种状态

API中Thread.State给出的线程状态如下:

【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
  1. 新建(new):线程对象被创建后就进入了新建状态。如:Thread thread = new Thread();
  2. 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而启动该线程。如:thread.start(); 处于就绪状态的线程随时可能被CPU调度执行。
  3. 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权限,暂时停止运行。直到线程进入就绪状态,才有机会进入运行状态。阻塞的三种情况:

    ​ 1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。

    ​ notify();随机叫醒同一把锁的线程;

    ​ notifyAll();叫醒所有使用同一把锁的线程

    ​ 2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。

    ​ 3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。

  5. 死亡状态(Dead):线程执行完了或因异常退出了run()方法,该线程结束生命周期。

    ​ 正常结束

    ​ 关闭JVM

十二、带返回值的线程Callable

Runnable 与 Callable

接口定义 
//Callable接口
public interface Callable<V> {
    V call() throws Exception;
}
//Runnable接口
public interface Runnable {
    public abstract void run();
}
           

Callable使用步骤

  1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
    @Override
    public <T> call() throws Exception {
        return T;
    }
}
           
  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
  1. 通过Thread,启动线程

Runnable 与 Callable的相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

Runnable 与 Callable的不同点

  • Runnable没有返回值;Callable可以返回执行结果
  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出

Callable获取返回值

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。

十三、线程池

实际用得不多,Java中都是多线程。

线程池 Executors

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

线程池的好处

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

Java中的四种线程池 . ExecutorService

1、缓存线程池

长度没有限制

步骤:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在,则创建线程 并放入线程池, 然后使用

2、定长线程池

长度是指定的数值

步骤:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

3、单线程线程池

步骤:

  1. 判断线程池 的那个线程 是否空闲
  2. 空闲则使用
  3. 不空闲则等待池中的单个线程空闲后使用

4、周期性任务定长线程池

步骤:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务

  • 定时执行

    参数1. runnable类型的任务

    参数2. 时长数字

    参数3. 时长数字的单位

  • 周期执行

    参数1. runnable类型的任务

    参数2. 时长数字(延迟执行的时长)

    参数3. 周期时长(每次执行的间隔时间)

    参数4. 时长数字的单位

十四、Lambda表达式

Lambda表达式是函数式编程思想,Java8版本引入的

  • 面向对象:提倡创建对象解决问题
  • 函数式编程思想:不关注过程,只注重结果

例1:实现Runnable打印哈哈哈哈哈

  1. 面向对象:把打印“哈哈哈哈哈”这件事封装在一个任务的对象里
/**
 * @author Elvira
 * @date 2020/10/14 0:04
 * @description 面向对象
 */
public class LambdaTest {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable(){

            @Override
            public void run() {
                System.out.println("哈哈哈哈哈");
            }
        });
        t.start();
    }
}
           
  • 运行结果:
    【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
  1. 函数式思想:关注方法,让代码更简单

    () -> System.out.println("哈哈哈哈哈")

    把这段代码传给Thread()。
    • ()里是需要传的参数,没有就不写
    • ->后面是方法中的内容
Thread t = new Thread(() -> System.out.println("哈哈哈哈哈"));
t.start();
           
  • 运行结果:

    和上面一大段代码一模一样

注意:如何使用Lambda表达式

⭐接口中只有一个抽象方法

例2:自定义接口打印sum

  1. 面向对象
/**
 * @author Elvira
 * @date 2020/10/14 0:15
 * @description 算数和
 */
public class LambdaTest1 {
    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);
    }
}
           
  • 运行结果:
【4-5】 《Java中多线程重点》——继承Thread、实现Runnable、死锁、线程池、Lambda表达式多线程总结
  1. 函数思想
print((int x, int y) -> {
    return x + y;
},100,200);
           
  • 运行结果:

    与上面一致

总结

这篇文章的重点部分除了线程池,其他都很重要,尤其是如何解决死锁问题,更是重中之重。

继续阅读