介绍
基本概念
-
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
-
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程(被称为:生命周期)。
比如:运行中的QQ,运行中的视频播放器等…
-
程序是静态的,进程是动态的。
-
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
-
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间(因为它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患)。
单核 CPU 与 多核 CPU 的理解
-
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他 “挂起”(晾着他,等他想通了,准备好了钱,再去交费)。但是因为CPU时间单元特别短,所以一般感觉不出来。
在使用单核 CPU 打开多个软件的时候,看似像多线程,其实还是单线程模式;它只是让一个软件执行一段时间,然后让另一个软件执行一段时间,以此进行切换使用(因为CPU时间单元特别短,因此感觉不出来)
-
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
-
一个 Java 应用程序 java.exe,其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程。
注意:如果发生异常,就会影响到主线程!
并行 与 并发
-
并行:多个 CPU 同时执行多个任务。比如:多个人同时做不同的事。
-
并发:一个 CPU (采用时间片) 同时执行多个任务。比如:秒杀、多个人做同一件事。
例如:打篮球的时候如果是有多个篮球场,每一队人各玩各的,那么这种情况就叫做 ”并行“;如果是一队人拿着球玩来玩去,那么就叫做 “并发” !
使用多线程的优点
以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验(比如:在听音乐的同时打开网页、文档等…)。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
什么时候需要多线程?
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
线程的分类(了解)
Java 中的线程分为两类:一种是守护线程,一种是用户线程。
-
它们在几乎每个方面都是相同的,唯一的区别是判断 JVM 何时离开。
-
守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程。
-
Java 垃圾回收就是一个典型的守护线程。
-
若 JVM 中都是守护线程,当前 JVM 将退出。
内存结构
- 线程可以细化为多个线程
- 每个线程,拥有自己独立的:栈、程序计数器
- 多个线程,共享同一个进程中的结构:方法区、堆
线程的创建与使用
public class Sample {
public void method1(String str) {
System.out.println(str);
}
public void method2(String str) {
method1(str);
}
public static void main(String[] args) {
Sample s = new Sample();
s.method2("hello!");
}
}
注意:以上这种方法调方法的方式并不是多线程(但是很多人会误解,所以这里先给大家说明)
JDK 1.5 之前创建线程的两种方式
-
继承 Thread 类的方式
-
实现 Runnable 接口的方式
方式一、继承 Thread 类
特性
-
每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把run()方法的主体称为线程体。
-
通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run()
实现步骤
- 创建一个 Thread 类的子类
- 重写 Thread 类的 run() 方法(将此线程的操作声明在 run() 中)
- 创建 Thread 类的子类的对象
- 通过此对象调用 start() 启动线程
案例一
遍历 100 以内的所有偶数
package com.laoyang.test.day01;
// 创建一个 Thread 类的子类
public class MyThread extends Thread {
// 重写 Thread 类的 run() 方法
@Override
public void run() {
int conditions = 100;
for (int i = 1; i <= conditions; i++) {
if (i % 2 == 0) {
//Thread.currentThread().getName():获取当前线程的名称
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
package com.laoyang.test.day01;
public class ThreadTest {
public static void main(String[] args) {
// 创建 Thread 类的子类的对象
MyThread myThread = new MyThread();
// 通过此对象调用 start() 启动线程
myThread.start();
// 问题一:我们不能直接通过调用 run() 的方式启动线程
// myThread.run();
/*
问题二:在启动一个线程来遍历100以内的偶数
不可以让已经 start() 的线程去执行,会报:IllegalThreadStateException(非法线程状态异常)
*/
//myThread.start();
// 我们需要重写创建一个线程的对象创建新的线程执行新的任务
MyThread myThread1 = new MyThread();
myThread1.start();
// Hello World 打印操作是在 main() 主线程中执行的,根据各自 cpu 的处理方式来进行处理,大家可以运行查看效果
System.out.println("Hello World!");
System.err.println(Thread.currentThread().getName());
}
}
start()作用:① 启动当前线程 ② 调用当前线程的 run()
问题:
能不能直接通过调用 run() 的方式启动线程?
不可以,因为 run() 只是一个方法,如果直接进行调用,那么就不是创建一个新的线程来执行任务了
可以在启动一个线程来遍历100以内的偶数吗?
不可以,因为一个线程只能执行一个任务,如果用一个线程去执行多个任务,那么就会导致IllegalThreadStateException异常,如果想要同时执行多个任务,那么可以创建多个 Thread 子类的对象,然后调用 start() 方法来创建一个新的线程用于执行对应的任务。
详情请看案例代码
案例二
创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历奇数
package com.laoyang.test.day01;
public class ThreadDemo {
public static void main(String[] args) {
// 方式一:创建各自操作的子类对象启动线程
// Even even = new Even();
// even.start();
//
// Odd odd = new Odd();
// odd.start();
// 方式二:创建 Thread 类的匿名子类的方式进行使用
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
// 偶数,采用方式二的话就可以不用在创建子类对象调用 start() 启动线程了
class Even extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
// 奇数
class Odd extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
Thread 类中的常用方法
- start():启动单前线程,调用当前线程的 run()
- run():通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前 cpu 的执行权
- join():在线程 A 中调用线程 B 的 join() 方法,此时线程 A 进入阻塞状态,直到线程 B 完全执行完以后,线程 A 才会结束阻塞状态。
- stop():已过时;当执行此方法时,强制结束当前线程
- sleep(long millis):让当前线程 “睡眠” 指定的 millis毫秒;在指定的 millis 时间内,当前线程是 “阻塞” 状态
- isAlive():判断当前线程是否存活
…
案例
package com.laoyang.test.day01;
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread helloThread = new HelloThread();
/*
默认线程名为 Thread
注意:修改线程名需要在调用 start() 之前进行
*/
helloThread.setName("HelloThread");
helloThread.start();
// 通过构造器的方式给线程进行命名
HelloThread thread = new HelloThread("构造器");
thread.start();
// 给当前的主线程修改线程名
Thread.currentThread().setName("main");
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
// join() 方法的使用
try {
helloThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// isAlive() 方法的使用
System.out.println(helloThread.isAlive());
}
}
class HelloThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
// sleep() 方法的使用
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 20 == 0) {
/*
yield() 方法的使用
Ps:因为释放了当前 cpu 的执行权,所以下一次执行的可能就是其它的线程(也有可能还是执行当前线程)
*/
yield();
}
}
}
public HelloThread() {}
public HelloThread(String name) {
super(name);
}
}
yield() 方法执行效果
…其它效果请大家自行测试查看
线程的优先级
- 等级
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5(默认优先级) - 如何获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int newPriority):设置线程的优先级
注意:高优先级的线程要抢占低优先级线程的 cpu 执行权,但是只是从概率上来说的;高优先级的线程高概率的情况下被执行,但是并不意味着只有当高优先级的线程执行完以后低优先级的线程才执行!
案例
package com.laoyang.test.day01;
public class ThreadPriority {
public static void main(String[] args) {
MyThreadPriority priority = new MyThreadPriority();
priority.setName("Priority");
// 给分线程设置优先级
priority.setPriority(Thread.MAX_PRIORITY);
priority.start();
Thread.currentThread().setName("main");
// 给主线程设置优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}
class MyThreadPriority extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}
练习
创建三个窗口卖票,总票数为100张
package com.laoyang.test.day01;
public class WindowThreadTest {
public static void main(String[] args) {
Window windowA = new Window();
Window windowB = new Window();
Window windowC = new Window();
windowA.setName("窗口1");
windowB.setName("窗口2");
windowC.setName("窗口3");
windowA.start();
windowB.start();
windowC.start();
}
}
class Window extends Thread {
//票数
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
// 因为继承了 Thread 类,所以可以直接使用 getName() 等方法
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
注意
因为使用继承 Thread 类的子类对象来创建线程并执行,每一次都需要创建一个新的子类对象,所以需要将该 ticket 设置为静态的,否则每一个线程都会从 100-1 进行卖票操作
Ps:该练习暂时还存在一点线程安全问题,后面会跟大家说明如何解决
方式二:实现 Runnable 接口
实现步骤
- 创建一个实现了 Runnable 接口的类
- 实现类去实现 Runnable 中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器中,然后创建 Thread 类的对象
- 通过 Thread 类的对象调用 start() 启动线程
案例
package com.laoyang.test.day01;
public class RunnableTest {
public static void main(String[] args) {
// 3.创建实现类的对象
MyRunnable runnable = new MyRunnable();
// 4.将此对象作为参数传递到 Thread 类的构造器中,然后创建 Thread 类的对象
Thread threadA = new Thread(runnable);
threadA.setName("AAA");
/*
5.通过 Thread 类的对象调用 start() 启动线程
调用了 Runnable 类型的 target 的 run()(详细可见源码)
*/
threadA.start();
// 在启动一个线程,遍历 100 以内的偶数
Thread threadB = new Thread(runnable);
threadB.setName("BBB");
threadB.start();
}
}
// 1.创建一个实现了 Runnable 接口的类
class MyRunnable implements Runnable {
// 2.实现类去实现 Runnable 中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
练习
创建三个窗口卖票,总票数为100张
package com.laoyang.test.day01;
public class WindowRunnableTest {
public static void main(String[] args) {
MyWindow window = new MyWindow();
Thread threadA = new Thread(window);
Thread threadB = new Thread(window);
Thread threadC = new Thread(window);
threadA.setName("窗口1");
threadB.setName("窗口2");
threadC.setName("窗口3");
threadA.start();
threadB.start();
threadC.start();
}
}
class MyWindow implements Runnable {
// 票数
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
注意
使用实现 Runnable 的方式实现卖票系统时不用将票数设置为 static 的,因为所有的线程都是共用一个Runnable实现类的;每一次执行都是修改 MyWindow 类中的值,A 修改完了给 B,B 修改完了给 C…以此执行
这个练习主要是为了让大家了解一下 Thread 和 Runnable 对于问题的实现方式,且目前都是存在线程安全问题的…
比较创建线程的两种方式
开发中:优先选择实现 Runnable 的方式
原因:
- 实现的方式没有类的单继承的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况
二者的联系:Thread 类实现了 Runnable 中的 run() 方法
Thread 源码:
public class Thread implements Runnable { ... }
相同点:两种方式都需要重写 run() 方法,将线程要执行的逻辑声明在 run() 方法中
线程的生命周期
Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步
例子
模拟火车站售票程序,开启三个窗口售票。
也就是上面我们写的那个售票功能,再此我们将会完善之前的安全问题
说明
-
问题:卖票过程中,出现了重票、错票(线程安全问题)
-
原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作当前车票(导致错票、重票等情况)
-
解决方法:当一个线程 A 在操作 ticket 的时候,其他线程不能参与进来,知道线程 A 操作完 ticket 时,其他线程才可以操作 ticket;
这种情况即使线程 A 出现阻塞,其他的线程一样是不能参与进来的!!!
-
在 JAVA 中,我们通过同步机制来解决线程安全问题
方式一:同步代码块
synchronized (同步监视器){ // 需要被同步的代码 }
说明:
-
操作共享数据的代码,即需要被同步的代码!
-
共享数据:多个线程共同操作的变量(数据);比如卖票案例中的 ticket
-
同步监视器(俗称:锁),任何一个类的对象都可以充当锁
要求:多个线程必须要共用一把锁
方式二:同步方法
权限修饰符 synchronized 返回类型 方法名() { // 代码块 }
说明:
- 如果操作共享数据的代码完整的声明在一个方法内,我们就可以将此方法声明为同步的
-
同步的好处与坏处
-
好处:同步的方式,解决了线程的安全问题
-
坏处:操作同步代码时,只能有一个线程参与,其他的线程等待,相当于是一个单线程的过程(也可以理解为给线程限制了执行的先后顺序,线程必须依次执行)
同步机制中的锁
synchronized 的锁是什么?
-
任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。
-
同步方法的锁:静态方法(类名.class)、非静态方法(this)
-
同步代码块:自己指定,很多时候也是指定为 this 或 类名.class
注意:
-
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
-
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
方式一、同步代码块
修改之前的卖票系统,使其更加完善!
Runnable 方式实现
说明:在实现 Runnable 接口创建多线程的方式中,可以考虑使用 this 充当同步监视器(建议还是使用对象的方式)
package com.laoyang.test.day02;
public class SynchronizeRunnable {
public static void main(String[] args) {
TheTicketWindow window = new TheTicketWindow();
Thread threadA = new Thread(window);
Thread threadB = new Thread(window);
Thread threadC = new Thread(window);
threadA.setName("窗口1");
threadB.setName("窗口2");
threadC.setName("窗口3");
threadA.start();
threadB.start();
threadC.start();
}
}
class TheTicketWindow implements Runnable {
// 总票数
private int ticket = 100;
//要求:多个线程必须要共用一把锁
Object object = new Object();
@Override
public void run() {
while (true) {
/*
使用synchronize时需要注意,如果共享数据太少的话,可能实现不了想要的效果!
比如这里的票数只有 100,一下就可以被执行完,所以配置稍微好一点的电脑都只需要一个线程就能执行完了...基本看不出什么效果
Ps:看不出效果就比如只有一个窗口在卖票,如果想看效果的话票数大概需要调到 10000 左右才可以
其它查看效果的方式:将sleep()方法放在synchronize的外面,即可看到三个窗口卖票的效果
*/
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//synchronized(object) {
//使用 this 的时候一定要保证锁的唯一性!!!否则很容易就会出现线程安全问题!
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
使用 synchronized 时需要注意,如果共享数据太少的话,可能实现不了想要的效果!
比如这里的总票数只有 100,一下就可以被执行完,所以配置稍微好一点的电脑只用一个 CPU 就可以完成…基本看不出什么效果(最后就会出现只有一个窗口在卖票的情况)
查看正常效果的方式:
将总票数设置为 10000 左右
将 sleep() 方法放在 synchronized 的外面,即可看到三个窗口卖票的效果(以上代码注释部分)
Thread 方式实现
说明: 在继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,可以考虑使用当前类充当同步监视器!
package com.laoyang.test.day02;
public class SynchronizeThread {
public static void main(String[] args) {
WindowA windowA = new WindowA();
WindowA windowB = new WindowA();
WindowA windowC = new WindowA();
windowA.setName("窗口1");
windowB.setName("窗口2");
windowC.setName("窗口3");
windowA.start();
windowB.start();
windowC.start();
}
}
class WindowA extends Thread {
// 票数
private static int ticket = 100;
/*
此处的同步监视器(锁)需要设置为 static 的,因为 Thread 类的方式是需要创建多个子类对象才能使用多个线程
如果不将 object 设置为静态,那么每一个子类对象使用的都是自己的锁,而不是同一个
*/
private static Object object = new Object();
@Override
public void run() {
while (true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
如果此处的参数使用 this 代替,那么就表示当前唯一的 WindowA 的对象;
而此处创建了三个子类对象各不相同,所以就导致每个锁都不一样(就会导致线程不安全)
所以这种方式请大家慎重使用(必须确保锁是唯一的!!!)
*/
//synchronized (this) {
//synchronized (object) {
// 类在执行的时候只会被加载一次,所以以下这种方式也是可用的
synchronized (WindowA.class) {
if (ticket > 0) {
// 因为继承了 Thread 类,所以可以直接使用 getName() 等方法
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
- 同步监视器必须确保使用的是同一个!!!
方式二、同步方法
说明:如果操作共享数据的代码完整的声明在一个方法内,我们就可以将此方法声明为同步的
Runnable 方式实现
package com.laoyang.test.day02;
/**
* @ClassName SynchronizedCodeBlock
* @Description: 使用同步方法解决实现 Runnable 接口的安全问题
* @Author Laoyang
* @Date 2021/8/27 11:07
*/
public class SynchronizedCodeBlockA {
public static void main(String[] args) {
TheTicketWindowA window = new TheTicketWindowA();
Thread threadA = new Thread(window);
Thread threadB = new Thread(window);
Thread threadC = new Thread(window);
threadA.setName("窗口1");
threadB.setName("窗口2");
threadC.setName("窗口3");
threadA.start();
threadB.start();
threadC.start();
}
}
class TheTicketWindowA implements Runnable {
// 总票数
private int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
show();
}
}
/**
* 同步方法中的同步监视器:this
*/
public synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
Thread 方式实现
package com.laoyang.test.day02;
/**
* @ClassName SynchronizedCodeBlockB
* @Description: 使用同步方法解决Thread卖票系统的线程安全问题
* @Author Laoyang
* @Date 2021/8/29 10:49
*/
public class SynchronizedCodeBlockB {
public static void main(String[] args) {
WindowB windowA = new WindowB();
WindowB windowB = new WindowB();
WindowB windowC = new WindowB();
windowA.setName("窗口1");
windowB.setName("窗口2");
windowC.setName("窗口3");
windowA.start();
windowB.start();
windowC.start();
}
}
class WindowB extends Thread {
// 票数
private static int ticket = 100;
@Override
public void run() {
while (true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
show();
}
}
/**
* 错误写法:public synchronized void show() { ... }
* 原因:使用同步方法时,锁用的是 this,如果不把该方法设置为静态的,那么每次创建子类对象启动线程都是一个新的锁(静态资源只会加载一次)
* 正确写法:public static synchronized void show() { ... }
* 同步监视器:当前类(WindowB.class)
*/
public static synchronized void show() {
if (ticket > 0) {
// 因为继承了 Thread 类,所以可以直接使用 getName() 等方法
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
Thread 方式是因为每一次都是创建一个新的子类对象,所以如果不将对应的方法和值设置为静态的,就会导致线程不安全
同步方法总结
- 同步方法仍然涉及到同步监视器,只是不需要我们显式(手动)的声明
- 非静态的同步方法,同步监视器为:this
- 静态的同步方法,同步监视器为:当前类本身
死锁
死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方先放弃自己需要的同步资源,就形成了线程的死锁!
说明
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都会处于阻塞状态,无法继续执行
- 使用同步时,要避免出现死锁
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
单例模式之懒汉式
懒汉式本身是线程不安全的,接下来我们就使用同步机制将单例模式中的懒汉式完善成线程安全的(此处有两种方式,建议使用效率较高的一种)
package com.laoyang.test.day03;
/**
* @ClassName BankTest
* @Description: 使用同步机制将单例模式中的懒汉式改写为线程安全的
* @Author Laoyang
* @Date 2021/8/29 11:02
*/
public class BankTest {
}
class Bank {
private Bank() {}
private static Bank instance = null;
public static Bank getInstance() {
/*
方式一:效率较低
假设第一次有三条线程进入该方法,然后线程一优先进入然后将 instance 赋了值,后面的两个线程在进去的时候就会发现已经被赋值了,然后直接返回
然后第二次...第n次的线程进入该方法时,还是会进入到同步代码块中进行判断,一个接一个的进行判断返回,就会导致效率比较差
*/
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// return instance;
// }
/*
方式二:效率较高
假设第一次有三条线程进入该方法,然后线程一优先进入然后将 instance 赋了值,后面的两个线程在进去的时候就会发现已经被赋值了,然后直接返回
而第二次...第n次的线程进入该方法时,就会被外层的 if 直接判断掉,从而直接返回已有的数据,进一步的提升了性能!
*/
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
案例一 - 根据代码理解死锁
package com.laoyang.test.day03;
/**
* @ClassName DeadlockTest
* @Description: 线程死锁的问题
* @Author Laoyang
* @Date 2021/8/29 11:37
*/
public class DeadlockTest {
public static void main(String[] args) {
StringBuffer stringA = new StringBuffer();
StringBuffer stringB = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (stringA) {
stringA.append("a");
stringB.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (stringB) {
stringA.append("b");
stringB.append("2");
System.out.println(stringA);
System.out.println(stringB);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (stringB) {
stringA.append("c");
stringB.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (stringA) {
stringA.append("d");
stringB.append("4");
System.out.println(stringA);
System.out.println(stringB);
}
}
}
}).start();
}
}
此处如果没有线程安全问题的话,结果为:ab 12 abcd 1234(将 sleep() 方法去掉即可变为线程安全)
此处我们为了演示,所以加上了 sleep() 方法让大家更好的看到程序死锁后的效果
解析:
- 首先,第一个线程中的 A 和 B 执行完后,执行 sleep() 方法,进入阻塞状态,这个时候其它的线程就可能趁虚而入
- 然后,第二个线程就会开始执行,第二个线程执行完 A 和 B 也执行了 sleep() 方法,这个时候第二个线程也进入了阻塞状态
- 最后,第一个线程阻塞完后想去拿 B,但是当前还没有结束(也就是现在还在使用A);而第二个线程阻塞完后想去拿 A,但也还没有结束(也就是现在还在使用B),所以导致最后两个线程僵持不下(第一个线程想去拿 B,第二个线程想去拿 A),都等着对方先执行完(就比如一个坑位空着,然后两个人去抢,最后一直挤在外面,谁都进不去)
案例二
package com.laoyang.test.day03;
class A {
// 同步监视器:a
public synchronized void foo(B b) {
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法"); // ③
b.last();
}
// 同步监视器:a
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
// 同步监视器:b
public synchronized void bar(A a) {
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法"); // ④
a.last();
}
// 同步监视器:b
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
线程同步方式三:Lock 锁(JDK5.0新增)
同步方式优先使用顺序:
Lock > 同步代码块(已经进入方法,分配了相应的资源 > 同步方法(在方法体之外)
Runnable 方式
package com.laoyang.test.day04;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName LockTest
* @Description: 解决线程安全的方式三:Lock锁(JDK5.0 新增的方式)
* @Author Laoyang
* @Date 2021/8/29 15:33
*/
public class LockRunnable {
public static void main(String[] args) {
WindowC window = new WindowC();
Thread threadA = new Thread(window);
Thread threadB = new Thread(window);
Thread threadC = new Thread(window);
threadA.setName("窗口一");
threadB.setName("窗口二");
threadC.setName("窗口三");
threadA.start();
threadB.start();
threadC.start();
}
}
class WindowC implements Runnable {
private int ticket = 100;
// 1. 实例化 ReentrantLock
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. 调用锁定方法:lock()
reentrantLock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ",票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
// 3. 调用解锁方法:unlock()
reentrantLock.unlock();
}
}
}
}
Thread 方式
package com.laoyang.test.day04;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName LockThread
* @Description:解决线程安全的方式三:Lock锁(JDK5.0 新增的方式)
* @Author Laoyang
* @Date 2021/8/29 16:13
*/
public class LockThread {
public static void main(String[] args) {
WindowD windowA = new WindowD();
WindowD windowB = new WindowD();
WindowD windowC = new WindowD();
windowA.setName("窗口一");
windowB.setName("窗口二");
windowC.setName("窗口三");
windowA.start();
windowB.start();
windowC.start();
}
}
class WindowD extends Thread {
private static int ticket = 100;
// 1. 实例化 ReentrantLock
private static ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. 调用锁定方法:lock()
reentrantLock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ",票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
// 3. 调用解锁方法:unlock()
reentrantLock.unlock();
}
}
}
}
面试题
-
synchronized 与 lock 异同?
相同点:二者都可以解决线程安全问题
不同点:synchronized 在执行完相应的同步代码以后,自动的释放同步监视器;
lock 需要手动的启动同步(lock()) ,同时结束同步也需要手动的实现(unlock()) -
如何解决线程安全问题?有几种方式?
答:可以使用 Lock、同步方法、同步代码块的方式解决线程安全问题。有三种
练习
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
分析
问题 | 分析 |
---|---|
是否是多线程问题? | 是,两个储户线程 |
是否有共享数据? | 是,账户(或账户余额) |
是否有线程安全问题? | 是 |
需要考虑如何解决线程安全问题? | 使用同步机制(有三种方式) |
package com.laoyang.test.day05;
/**
* @ClassName AccountTest
* @Description:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
* @Author Laoyang
* @Date 2021/8/29 15:59
*/
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(0);
Customer customerA = new Customer(account);
Customer customerB = new Customer(account);
customerA.setName("AAA");
customerB.setName("BBB");
customerA.start();
customerB.start();
}
}
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
/**
* 存钱
*/
public synchronized void deposit(double amt) {
if (amt > 0) {
balance += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "存款成功,当前余额为:" + balance);
}
}
}
class Customer extends Thread {
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}
线程通信
涉及到的三个方法
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被 wait 的一个线程;如果有多个线程被 wait,就会唤醒优先级高的那一个线程。
- notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。
说明
- wait()、notify()、notifyAll() 三个方法必须使用在同步代码块或同步方法中(不能在 lock 中使用!!!)
- wait()、notify()、notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器!(否则就会出现 IllegalMonitorStateException 异常)
- wait()、notify()、notifyAll() 三个方法是定义在 java.lang.Object 类中的(因为需要保证任何一个类的对象都可以进行调用)
案例
使用两个线程打印 1-100。线程1, 线程2 交替打印
package com.laoyang.test.day06;
/**
* @ClassName CommunicationTest
* @Description: 线程通信(案例:使用两个线程打印 1-100。线程1, 线程2 交替打印)
* @Author Laoyang
* @Date 2021/8/29 16:22
*/
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread threadA = new Thread(number);
Thread threadB = new Thread(number);
threadA.setName("线程一");
threadB.setName("线程二");
threadA.start();
threadB.start();
}
}
class Number implements Runnable {
private int number = 1;
private Object object = new Object();
@Override
public void run() {
while (true) {
//synchronized (this) {
synchronized (object) {
// 如果有多个(两个以上)线程的时候可以使用 notifyAll() 方法来唤醒多个线程
// notifyAll();
/*
因为此处我们只使用了两个线程,可以互相进行唤醒,所以使用 notify() 方法就可以了(如果有两个以上的线程就会先唤醒优先级高的线程)
如果同步监视器为 this,那么 wait() 和 notify() 方法就是使用 “this.” 的(可省略)
但是如果使用其它对象作为同步监视器,那么就需要使用对应的对象来引用
*/
//notify();
object.notify();
if (number <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
// 让调用了 wait() 方法的线程进入阻塞状态
try {
//wait();
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
经典例题 - 生产者/消费者问题
生产者(Productor) 将产品交给店员 (Clerk),而消费者 (Customer) 从店员处取走产品,店员一次只能持有固定数量的产品 (比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析
问题 | 分析 |
---|---|
是否有多线程问题? | 是,生产者线程,消费者线程 |
是否有共享数据的问题? | 是,产品(或产品) |
如何解决线程的安全问题? | 使用同步机制,有三种方式 |
是否设计到线程通信? | 是 |
package com.laoyang.test.day06;
/**
* @ClassName ProiductTest
* @Description: 经典例题:生产者/消费者问题(线程通信的应用)
* @Author Laoyang
* @Date 2021/8/29 17:06
*/
public class ProiductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk(0);
Productor productor = new Productor(clerk);
Customer customer = new Customer(clerk);
//Customer customerA = new Customer(clerk);
productor.setName("生产者");
customer.setName("消费者");
//customerA.setName("消费者A");
productor.start();
customer.start();
customerA.start();
}
}
/**
* 店员
*/
class Clerk {
// 产品数量
private int count = 0;
public Clerk(int count) {
this.count = count;
}
/**
* 生产产品
*/
public synchronized void produceProduct() {
if (count < 20) {
// 先 ++ 的原因是因为如果开始只有 0 个产品,那么就会打印开始生产第0个产品,所以我们先++,打印出来的就是 1
count++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + count + "个产品");
/*
大概逻辑:如果此时还没有产品,那么消费者线程就会进入阻塞状态(wait())
然后需要等待生产者生产出产品后在释放消费者线程
*/
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 消费产品
*/
public synchronized void constumeProduct() {
if (count > 0) {
// 这里要注意,生产是先 ++,而消费是后 --,是因为要等消费者消费完了之后才会减少一个产品,如果先 --,那么数量就会不太对
System.out.println(Thread.currentThread().getName() + ":开始消费第" + count + "个产品");
count--;
/*
大概逻辑:当生产者生产满了之后(20个),就会进入阻塞状态(wait())
然后需要等待消费者消费产品后才会释放生产者线程
*/
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
class Productor extends Thread {
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品");
while (true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
/**
* 消费者
*/
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品");
while (true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.constumeProduct();
}
}
}
大家可以试着多添加几个线程,或者将生产的时间和消费的时间设置为不同的,比如1秒生产一个产品,2秒消费一个产品等…可以更加客观的看到效果
面试题
sleep() 和 wait() 的异同?
-
相同点:一旦执行方法,都可以使当前线程进入阻塞状态
-
不同点:
-
两个方法声明的位置不同:Thread 类中声明 sleep();Object 类中声明 wait()
-
调用的范围不同:sleep() 可以在任何需要的场景下调用;wait() 必须在同步代码块或同步方法中调用
-
关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,而 wait() 会释放锁
Ps:锁就是同步监视器
-
其它创建线程的方式 - JDK5.0之后
方式三、实现 Callable 接口
如何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式强大?
- call() 方法可以有返回值
- call() 方法可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable 是支持泛型的
在开发中一般使用 Callable 的频率比 Runnable 要高(因为更加方便好用)
步骤
- 创建一个 Callable 接口的实现类
- 实现 call() 方法,将此线程需要执行的操作声明在 call() 中
- 创建 Callable 接口实现类的对象
- 将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 对象
- 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start() 方法启动线程
- 获取 Callable 中 call() 方法的返回值(可有可无)
案例
package com.laoyang.test.day07;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @ClassName CallableTest
* @Description: 创建线程的方式三:实现 Callable 接口
* @Author Laoyang
* @Date 2021/8/29 17:39
*/
public class CallableTest {
public static void main(String[] args) {
// 3. 创建 Callable 接口实现类的对象
NumThread numThread = new NumThread();
// 4. 将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 对象
FutureTask futureTask = new FutureTask(numThread);
// 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start() 方法启动线程
new Thread(futureTask).start();
// 6. 获取 Callable 中 call() 方法的返回值(可有可无,根据需求而定)
try {
/*
get() 方法的返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值
如果需要获取 call() 方法的返回值就可以使用 get() 方法,如果不需要则可不写以下代码
*/
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 1. 创建一个实现 Callable 的实现类
class NumThread implements Callable {
// 2. 实现 call() 方法,将此线程需要执行的操作声明在 call() 中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
sum += i;
}
}
// 如果不想要返回值,则可以直接返回 null
return sum;
}
}
方式四、使用线程池
说明
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。因此我们就可以使用线程池的方式来进行弥补
思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- …
案例
package com.laoyang.test.day07;
import java.util.concurrent.*;
/**
* @ClassName ThreadPoolTest
* @Description: 创建线程的方式四:线程池
* @Author Laoyang
* @Date 2021/9/2 10:35
*/
public class ThreadPoolTest {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
// 设置线程池属性
System.out.println(executorService.getClass()); //获取指定对象是哪个类创造的
executor.setCorePoolSize(20); // 核心池大小
executor.setMaximumPoolSize(15); // 最大线程数
// 2. 执行指定的线程的操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
executorService.execute(new NumberThreadA()); // 适合使用于 Runnable
executorService.execute(new NumberThreadB()); // 适合使用于 Runnable
executorService.submit(new NumberThreadC()); // 适合使用于 Callable
// 3. 关闭连接池
executorService.shutdown();
}
}
class NumberThreadA implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class NumberThreadB implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 3 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class NumberThreadC implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + ":" + sum);
return null;
}
}
关于 Runnable 和 Callable 的方式以上都有例子,更丰富内容请大家自行搜索
面试题
有几种创建线程的方式?
四种:Thread、Runnable、Callable、线程池
部分小结
-
有哪些释放锁的操作
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行
- 当前线程在同步代码块、同步方法中出现了未处理的 Error 和 Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
-
有哪些不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
- 线程执行同步代码块时,其它线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)
要尽量避免使用 suspend() 和 resume() 来控制线程
-
Callable 和 Runnable 的区别?
-
call() 是有返回值的
-
call() 可以抛出异常,可以被外面的操作捕获,获取异常信息
-
Callable 支持泛型
-
-
使用线程池的好处
-
提高响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中的线程,不需要每次创建)
-
便于线程管理(可以对线程的属性进行配置)
-
-
Java 解决线程安全:同步机制
-
同步代码块
共享数据:多个线程共同操作的变量
同步监视器(锁):任何一个类的对象都可以作为同步监视器,但是在多个线程中必须使用同一个同步监视器 -
同步方法
静态方法:锁为 当前类本身
非静态方法:锁为 this -
Lock 锁 - JDK5.0新增
需要手动启动同步和关闭同步
优先使用顺序(根据性能):Lock 锁 > 同步代码块 > 同步方法
-
-
生命周期:新建 - 就绪 - 运行 - 阻塞 - 死亡
-
生命周期关注两个概念:状态和相应的方法
-
关注:状态A -> 状态B(状态改变的时候哪些方法执行了(通常被称为 “回调方法”))
某个方法主动调用(导致状态发现改变):状态A -> 状态B -
阻塞:临时状态,不可以作为最终状态
-
死亡:最终状态
-