第八章:多线程
8-1 基本概念:程序、进程、线程 (了解即可)
程序(program):一段静态代码,静态对象。为了完成特定任务、用某种语言编写的一组指令的集合
进程(process):是程序的一次执行过程,或是在运行的一个程序
线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)
并行与并发:
并行:多个 CPU 同时执行多个任务
并发:一个 CPU 同时执行多个任务
并发:一个 CPU 同时执行多个任务 比如:秒杀、多个人做同一件事
8-2 线程的创建和使用
方式一、继承于 Thread 类
1、创建一个继承于 Thread 类的子类
2、重写 Thread 类的run()
3、创建 Thread 类的子类对象
4、通过此对象调用 start()
我们不能直接调用对象.run()来启动线程
不可以让已经 start 的线程再去执行一个新的线程;我们需要重新创建一个线程的对象
用代码来演示一下过程
/**
* 多线程的创建,方式一:继承于 Thread 类
* @author Jackson_kcw
* @create 2025-02-22
*/
public class ThreadTest {
public static void main(String[] args) {
//3、创建 Thread类的子类的对象
MyThread myThread = new MyThread();
//4、通过次对象调用 start() :启动当前线程、调用当前线程的 run()
myThread.start();
}
}
//1、创建一个继承于 Thread 类的子类
class MyThread extends Thread{
//2、重写 Thread 类的 run()
@Override
public void run() {
for (int i = 0; i <100 ; i++) {//输出 100以内的偶数
if(i%2==0){
System.out.println(i);
}
}
}
}
Thread 其中的方法
yield():释放当前 CPU 的执行权
join():在线程 a 中调用线程 b 的 join 方法,此时线程 a 进入阻塞状态,直到线程 b 完全执行完之后,线程 a 才会结束阻塞状态
stop(): 强制结束当前线程
sleep(long millitime):让当前线程“睡眠”指定的 millitime 毫秒,在指定的时间内,当前线程为阻塞状态
线程的优先级:
1、优先级分类
MAX_PRIORITY: 10
MIN_PRIORITY:1
NORM_PRIORITY:5
2、
getPriority():获取线程的优先级
setPriority(int p):设置线程优先级
注:高优先级只是从概率上来说更可能会先执行,但不意味着绝对
方式二、创建多线程的方式二:实现 Runnable 接口
1、创建一个实现了 Runnable 接口的类
2、实现类去实现 Runnable 的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
5、通过 Thread 类的对象调用 start()
代码例子:
package com.kcw.java1;
/**创建多线程的方式二:实现 Runnable 接口
* @author Jackson_kcw
* @create 2025-02-23
*/
public class ThreadTest1 {
public static void main(String[] args) {
//3、创建实现类的对象
MThread thread = new MThread();
//4、将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
Thread t1=new Thread(thread);
//5、通过 Thread 类的对象调用 start():启动线程;调用当前线程的 run()
t1.start();
}
}
//1、创建一个类实现了 Runnable 接口的方法
class MThread implements Runnable{
//2、实现类去实现 Runnable 中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i%2==0){
System.out.println(i);
}
}
}
}
创建线程的两种方式的比较
开发中:优先选择:实现 Runnable 接口的方式
原因:1、实现的方式没有类的单继承性的局限性
2、实现的方式更适合来处理多个线程共有数据的情况
联系:Thread 也是实现 Runnable
相同点:两者都需要重写 run 方法,将线程要执行的逻辑声明在 run()中
8-3 线程的生命周期
完整生命周期的几种状态
线程生命周期
8-4 线程的同步
线程安全问题:当某个线程在运行过程中,另一个线程进入运行状态,会影响到共享数据;如卖票 会出现负数情况
如何解决线程安全问题:当一个线程 a操作共享数据数据的时候,其他的线程不能参与进来,直到线程 a操作完共享数据,其他线程才可以开始操作共享数据,即使线程 a 出现阻塞,也不能改变
在 Java 中,我们通过同步机制,解决线程的安全问题
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:1、操作共享数据的代码即为 需要被同步的代码 ---->不能包含代码多了,也不能包少了
2、共享数据:多个线程共同操作的变量。比如卖票问题里面的 ticket
3、同步监视器:俗称锁。任何一个类的对象都可以充当锁
要求:多个线程必须要共用一把锁
补充:在实现 Runnable接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器
在继承 Thread 类创建多线程的方式中,慎用使用 this 充当同步监视器,可以考虑使用当前类充当同步监视器
方法二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步
1、同步方法仍然涉及到同步监视器,只是不需要我们显式声明
2、非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
同步方法的格式如下代码:show()方法的格式
package java1;
/**
* @author Jackson_kcw
* @Time 2025-02-23
*/
public class WindowTest4 {
public static void main(String[] args) {
Window4 w = new Window4();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Window4 implements Runnable{
private static int ticket=100;
@Override
public void run() {
while(true){
show();
}
}
public synchronized void show(){//同步方法 如果这个是继承 Thread 会创建几个对象,需要加 static 将此方法转为静态
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票:"+ticket);
ticket--;
}
}
}
同步的方式,解决了线程的安全;但是在操作同步代码时,只能一个线程参与,其他线程等待,效率会低一点
补充:之前的单例模式中的懒汉解决线程安全问题
mac 电脑中 ide 包裹代码块的快捷键为:option+command+T
/**使用同步机制将单例模式里面的懒汉式改写成线程安全的
* @author Jackson_kcw
* @Time 2025-02-23
*/
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance=null;
public synchronized Bank getInstance(){
//方式一:效率不高
// synchronized (Bank.class) {
// if(instance==null){
// instance=new Bank();
// return instance;
// }
// else return instance;
// }
//方式二:效率更高
if(instance==null){
synchronized (Bank.class) {
if(instance==null){
instance=new Bank();
}
}
}
return instance;
}
}
死锁问题
我们使用同步时,要避免死锁
解决线程安全问题的方式三:Lock 锁-------JDK5 新增的
例如:
package Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Jackson_kcw
* @Time 2025-02-23 PM4:52
*/
public class LockTest {
public static void main(String[] args) {
Window window=new Window();
Thread thread1=new Thread(window);
Thread thread2=new Thread(window);
Thread thread3=new Thread(window);
thread1.start();
thread2.start();
thread3.start();
}
}
class Window implements Runnable{
private int ticket=100;
//1、实例化 ReentrantLock
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while(true){
//2、调用锁定方法 lock
lock.lock();
try {
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票:"+ticket);
ticket--;
}
else break;
} finally {
//3、调用解锁方法 unlock()
lock.unlock();
}
}
}
}
面试题:synchronized 于 Lock 的异同
相同:两者都是解决线程安全的方法
不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监察器
Lock 需要手动的启动同步(lock()),同时结束同步也需要手动的实现解锁( unlock() )
优先使用的顺序
Lock–>同步代码块(已经进入了方法体,分配了相应资源)–>同步方法(在方法体之外)
如何解决线程安全问题?有哪几种方式
答:1、使用 synchronized 关键字,用于代码块或方法(即同步代码块、同步方法)
2、使用 ReentrantLock(可重入锁),lock()与 unlock()手动设置锁和解除锁
8-5 线程的通信
线程通信涉及到三个方法:
wait():使线程进入阻塞状态,并释放同步监视器
notify():唤醒被 wait 的一个线程,如果有多个线程被 wait,就唤醒优先级高的
notifyAll():唤醒所有 wait 的线程
以下见代码例子:
package Java2Communication;
/**线程通信的例子:使用两个线程打印 1-100.线程一、线程二交替打印
* @author Jackson_kcw
* @Time 2025-02-24 PM2:04
*/
public class Communication {
public static void main(String[] args) {
Number n1=new Number();
Thread t1=new Thread(n1);
Thread t2=new Thread(n1);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number=1;
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒 阻塞的线程
notify();
if(number<=100){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else break;
}
}
}
}
注意点:
1、wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中
2、这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException异常
3、这三个方法都定义在 Java.lang.Object 类中
面试题
sleep()和 wait()的异同:
同:一旦执行方法,都可以使当前的线程进入阻塞状态
异:1、两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()
2、调用的范围要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块
3、关于是否释放同步监视器的问题:如果两个方法都用在同步代码块或同步方法中,sleep 不会释放 锁(同步监视器),但是 wait()会释放锁
8-6 JDK5.0 新增线程创建方式
新增方式一:实现 Callable 接口
创建步骤:
1、创建一个实现 Callable 的实现类
2、实现 call 方法,将此线程需要执行的操作声明在 call()中
3、创建 Callable 接口实现类的对象
4、将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象
5、将 FutureTask 的对象作为参数传递到 Thread类的构造器中,创建 Thread 对象,并调用 start();
6、获取 Callable 中的 call 方法的返回值
在代码中如下:
package Java3;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**创建线程的实现方式三:实现 Callable接口
* @author Jackson_kcw
* @Time 2025-02-24 PM2:40
*/
public class ThreadNew {
public static void main(String[] args) {
//3、创建 Callable 接口实现类的对象
NumberThread numberThread = new NumberThread();
//4、将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象
FutureTask futureTask= new FutureTask(numberThread);
//5、将 FutureTask 的对象作为参数传递到 Thread类的构造器中,创建 Thread 对象,并调用 start();
new Thread(futureTask).start();
try {
//6、获取 Callable 中的 call 方法的返回值
//get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值
Object sum=futureTask.get();
System.out.println("总和为:"+sum);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
//1、创建一个实现 Callable 的实现类
class NumberThread implements Callable {
//2、实现 call 方法,将此线程需要执行的操作声明在 call()中
@Override
public Object call() throws Exception {
int sum=0;
for (int i = 1; i <=100 ; i++) {
if(i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
二、如何理解实现 Callable 接口的方式创建多线程比实现Runnable接口要强大
1、call()方法可以有返回值
2、call()方法可以抛出异常,被外面的操作捕获,获取异常信息
3、Callable 是支持泛型的
新增方式二:使用线程池
利用线程池方式创建线程的步骤:
1、提供指定线程数量的线程池
2、执行指定的线程的操作。需要提供实现 Runnable 或 Callable 接口的实现类
3、关闭连接池子
具体的代码例子
package Java3;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**创建线程的方式四:使用线程池的方式
* @author Jackson_kcw
* @Time 2025-02-24 PM3:31
*/
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//2、执行指定的线程的操作。需要提供实现 Runnable 或 Callable 接口的实现类
executorService.execute(new NumberThread2());//适合使用于 Runnable
executorService.submit(new FutureTask(new NumberThread1())); //适合适用于 Callable
executorService.shutdown();//3、关闭连接池子
}
}
class NumberThread1 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
return null;
}
}
class NumberThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
面试题:创建多线程的方式?四种
方式一、继承于 Thread 类
方式二、实现 Runnable 接口
方式三:实现 Callable 接口
方式四:使用线程池(实际开发中常用)