Java EE 初阶--多线程(一)

目录

一、认识多线程

1.1 概念

(1) 线程是什么

 (2)为啥要有线程?

 (3) 进程和线程的区别

(4)Java 的线程和操作系统线程的关系

 

1.2 创建线程

方法1 继承 Thread 类

方法2 实现Runnable接口

使用Runnable定义任务有啥好处?

其他变形:

1.3 Thread 类及常见方法

(1)Thread 的常见构造方法

 ​编辑

(2) Thread 的几个常见属性

 

(3)启动一个线程-start()

(4)中断一个线程

 (5) 线程等待

 (6) 获取当前线程

(7)休眠当前线程

1.4 线程的状态

二、多线程带来的风险--线程安全

2.1 什么是线程安全?

2.2 线程不安全的原因

(1) 修改共享数据

(2)线程是抢占是执行的

(3)原子性

(4)内存可见性

 (5)有序性

三、Synchronized&volatile&wait¬ify的使用

3.1 Sychronized锁

Sychronized的使用

 Sychronized的特性

 

3.2  volatile 关键字

3.3 wait 和 notify

wait()方法

notify()方法

notifyAll()方法


一、认识多线程

1.1 概念

(1) 线程是什么

一个线程就是一个 " 执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行着多份代码.

 举个栗子:

我们设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread )。

 (2)为啥要有线程?

首先 , " 并发编程 " 成为 " 刚需 ".
单核 CPU 的发展遇到了瓶颈 . 要想提高算力 , 就需要多核 CPU. 而并发编程能更充分利用多核 CPU
资源 .
有些任务场景需要 " 等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作 , 也需要用到并发编
.
其次 , 虽然多进程也能实现 并发编程 , 但是线程比进程更轻量 .
创建线程比创建进程更快 .
销毁线程比销毁进程更快 .
调度线程比调度进程更快 .
最后 , 线程虽然比进程轻量 , 但是人们还不满足 , 于是又有了 " 线程池 "(ThreadPool) " 协程 "
(Coroutine)
面试题:什么是并发?什么是并行?
并发:一会干这件事,一会干那件事(同时只能干一件事)
并行:一边干这件事,一边干那件事(真正意义上的同时进行)
在编程方面,我们并不区分是并行还是并发,统一称为并发编程。

 举个栗子:

 (3) 进程和线程的区别

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
比如多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别 人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

 

(4)Java 的线程和操作系统线程的关系

线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用( 例如 Linux pthread ).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装 .


1.2 创建线程

方法1 继承 Thread

(1)  继承 Thread 来创建一个线程类 .
public class Demo01_Thread {
    public static void main(String[] args) {
        // 创建自己定义的线程对象
        MyThread thread = new MyThread();
        // 执行这个线程 start方法是启动线程,并通知操作系统加入CPU调度
        thread.start();

    }
}

// 继承Thread类并实现run方法
class MyThread extends Thread {
    // run方法中的代码,就表示线程要执行的任务
    @Override
    public void run() {
        System.out.println("hello thread...");
    }
}
调用start()方法之后,JVM会调用系统API并在系统中生成一个PCB来执行run()方法中的代码

方法2 实现Runnable接口

(1)实现Runnable接口并重写run()
 

public class Demo03_Runnable {
    public static void main(String[] args) {
        // 创建Runnable对象
        MyRunnable runnable = new MyRunnable();
        // 创建线程
        Thread thread = new Thread(runnable);
        // 启动线程,参与CPU调度
        thread.start();
    }

}
// 实现Runnable接口
class MyRunnable implements Runnable {

    // 表示的是线程要执行的任务
    @Override
    public void run() {
        while (true) {
            System.out.println("生产皮包,金币+1...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用Runnable定义任务有啥好处?

  • 解耦:把定义线程,与定义任务分开(把不同的功能都给分开,如果要修改或查找相应的功能的时候可以直接在指定的位置查找)
  • 把创建线程,与定义任务分开,以便修改代码时,可以统一修改

其他变形:

  • 匿名内部类创建 Thread 子类对象
    // 使用匿名类创建 Thread 子类对象
    Thread t1=new Thread() {
        @Override
        public void run() {
            System.out.println("使用匿名类创建 Thread 子类对象");    
       }
    };
    • 匿名内部类创建 Runnable 子类对象
    // 使用匿名类创建 Runnable 子类对象
    Thread t2=new Thread(newRunnable() {
        @Override
        public void run() {
            System.out.println("使用匿名类创建 Runnable 子类对象");    
       }
    });
    • lambda 表达式创建 Runnable 子类对象
    Thread t3=new Thread(() ->System.out.println("使用匿名类创建 Thread 子类对象"));
    Thread t4=new Thread(() -> {
        System.out.println("使用匿名类创建 Thread 子类对象");
    });

    1.3 Thread 类及常见方法

    Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
    用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

(1)Thread 的常见构造方法

 

 Thread t1=new Thread();
Thread t2=new Thread(newMyRunnable());
Thread t3=new Thread("这是我的名字");
Thread t4=new Thread(newMyRunnable(), "这是我的名字");

(2) Thread 的几个常见属性

public class Demo10_Properties {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello thread...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "大圣");
        // 启动线程
        thread.start();

        // 打印Thread对象中的属性
        System.out.println("线程Id = " + thread.getId());
        System.out.println("线程名 = " + thread.getName());
        System.out.println("线程状态 = " + thread.getState());
        System.out.println("线程优先级 = " + thread.getPriority());
        System.out.println("线程是否后台 = " + thread.isDaemon());
        System.out.println("线程是否存活 = " + thread.isAlive());
        System.out.println("线程是否中断 = " + thread.isInterrupted());
    }
}

(3)启动一个线程-start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:行动起来!“,线程才真正独立去执行了。

调用 start 方法 , 才真的在操作系统的底层创建出一个线程 .

(4)中断一个线程

 举个栗子
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式
1. 通过是否中断的标志位
public class Demo13_Interrupted01 {
    // 定义一个中断标识
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello thread...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程任务结束");
        });
        // 启动线程
        thread.start();
        // 主线程休眠3秒,模拟子线程正在处理任务
        Thread.sleep(3000);
        // 设置中断标志位为true
        isQuit = true;
        // 让子线程先结束
        Thread.sleep(1000);
        System.out.println("主线程结束");

    }
}
2. 调用Thread类提供的 interrupted() 方法
public class Demo14_Interrupted02 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 注意判断条件
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 异常处理来中断线程
                    // 方式一:啥也不做
                    // 方式二:处理具体的逻辑
                    // 方式三:真正的中断
                    break;
                }
            }
            System.out.println("是否中断:" + Thread.currentThread().isInterrupted());
            System.out.println("线程任务结束");
        });
        // 启动线程
        thread.start();
        // 主线程休眠3秒,模拟子线程正在处理任务
        Thread.sleep(3000);
        // 中断线程,修改Thread中的中断标志
        thread.interrupt();
        // 让子线程先结束
        Thread.sleep(1000);
        System.out.println("主线程结束");
    }
}

 (5) 线程等待

举个栗子:
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnabletarget= () -> {
            for (inti=0; i<10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() 
                                       +": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedExceptione) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() +": 我结束了!");        
};
  Threadthread1=newThread(target, "李四");
        Threadthread2=newThread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");        
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
    }
}

 (6) 获取当前线程

   返回当前线程的引用

public class ThreadDemo {
    public static void main(String[] args) {
        Threadthread=Thread.currentThread();       
       System.out.println(thread.getName());
    }
}

(7)休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo {
    public static void main(String[] args) throwsInterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3*1000);
        System.out.println(System.currentTimeMillis());
    }
}

1.4 线程的状态

 

 


二、多线程带来的风险--线程安全

2.1 什么是线程安全?

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。

2.2 线程不安全的原因

(1) 修改共享数据

多个线程修改了同一个共享变量

多个线程修改不同的变量,不会出现线程安全问题。多个线程读取同一个变量,也不会出现线程安全的问题,单线程环境下也不会出现线程安全问题。

(2)线程是抢占是执行的

多个线程在CPU上调度是随机的,顺序是不可预知的

(3)原子性

什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

要么全都执行,要么全都不执行

(4)内存可见性

多线程环境下,某一线程修改的共享变量的值,另一个线程没有感受到最新的值

引入JAVA内存模型的概念JMM

1、主内存指的是硬件的内存条进程在启动的时候会申请一些资源,包括内存资源用来保存所有的变量,

2、工作内存指的是线程独有的内存空间,他们之间不能够相互访问,起到了线程之间内存隔离的作用,

3、JMM规定一个线程在修改某个变量的值时,必须把这个变量从主内存中加载到自己的工作内存,修改完成后再修,再刷新回主内存

4、每个工作内存之间是相互隔离的。

为什么要用JMM?

因为JAVA是一个跨平台的语言,把不同的计算设备和操作系统对内存的管理做了一个统一的封装。

 (5)有序性

编译过程,JVM调用本地接口CPU执行指令过程中指令的有序性。指令在特殊情况下会打乱顺序,并不是按程序员的预期去执行的。

 

 

三、Synchronized&volatile&wait&notify的使用

3.1 Sychronized锁

  •  加锁之后可以把多线程并发执行并成单线程串行执行
  • 由于是单线程执行的,所以第二个线程读到的值一定是第一个线程修改过的值,从而实现的内存可见
  • Sychronized可以解决原子性内存可见性,但是不能解决有序性问题

Sychronized的使用

synchronized 本质上要修改指定对象的 " 对象头 ". 从使用角度来看 , synchronized 也势必要搭配一个具体的对象来使用.
1) 直接修饰普通方法 : 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
    }
}
2) 修饰静态方法 : 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
    public synchronized static void method() {
    }
}
3) 修饰代码块 : 明确指定锁哪个对象 .
   锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
        }
    }
}
锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }    
   }
}

 Sychronized的特性

1) 互斥
synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待 .
2) 刷新内存
synchronized 的工作过程 :
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
3) 可重入

3.2  volatile 关键字

volatile 能保证内存可见性 有序性 volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 不保证原子性

代码在写入 volatile 修饰的变量的时候,
  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
  •  从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

保证内存可见性

创建两个线程 t1 t2
t1 中包含一个循环 , 这个循环以 flag == 0 为循环条件 .
t2 中从键盘读入一个整数 , 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候 , t1 线程结束 .
static class Counter {
    public int flag=0;
}
public static void main(String[] args) {
    Counter counter=new Counter();
    Thread t1=new Thread(() -> {
        while (counter.flag==0) {
            // do nothing
        }
        System.out.println("循环结束!");
    });
    Thread t2=new Thread(() -> {
        Scanner scanner=new Scanner(System.in);        
       System.out.println("输入一个整数:");
        counter.flag=scanner.nextInt();
    });
    t1.start();    
    t2.start();}

不保证原子性

increase 方法去掉 synchronized
count 加上 volatile 关键字 .
static class Counter {
    volatile public int count=0;
void increase() {
        count++;
    }
}
public static void main(String[] args) throwsInterruptedException {
    final Counter counter=new Counter();
  Thread t1=new Thread(() -> {
        for (int i=0; i<50000; i++) {
            counter.increase();
        }
    });
    Thread t2=new Thread(() -> {

        for (int i=0; i<50000; i++) {
            counter.increase();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);}
//此时可以看到, 最终 count 的值仍然无法保证是 100000.  

3.3 wait notify

由于线程之间是抢占式执行的 , 因此线程之间执行的先后顺序难以预知 .
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序 .
举个栗子:
球场上的每个运动员都是独立的 " 执行流 " , 可以认为是一个 " 线程 ".
而完成一个具体的进攻得分动作 , 则需要多个运动员相互配合 , 按照一定的顺序执行一定的动作 , 线程1 " 传球 " , 线程 2 才能 " 扣篮 ".

 完成这个协调工作, 主要涉及到三个方法

wait() / wait(long timeout): 让当前线程进入等待状态.

notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意 : wait, notify, notifyAll 都是 Object 类的方法 .

wait()方法

wait 做的事情:

使当前执行代码的线程进行等待. (把线程放到等待队列中)

释放当前的锁满足一定条件时被唤醒, 重新尝试获取这个锁 .
wait 要搭配 synchronized 来使用 . 脱离 synchronized 使用 wait 会直接抛出异常
wait 结束等待的条件 :
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
public static void main(String[] args) throwsInterruptedException {
    Object object=new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
    }
}

notify()方法

notify 方法是唤醒等待的线程.
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
        完,也就是退出同步代码块之后才会释放对象锁。\
wait()和notify()搭配使用
wait()是让线程进入休眠阶段,notify()去唤醒等待的线程 与锁强相关

 sleep(long)让线程进入休眠状态 在指定的时间内不会被调度到CPU上执行

 

 

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值