文章目录
1、多线程概述
1.1、线程与进程
进程: 进程是系统进行资源分配的最小单位,也是系统进行资源调度的基本执行单元。一个进程指的是内存中运行的一个应用程序,每个进程都有独立的内存空间。
线程: 线程是CPU调度的最小单位,是进程中的一个实体。一个进程可能有多条执行路径,每一条执行路径都是一个线程,它们共享一个内存空间,线程并发执行,线程之间可以自由切换,一个进程最少有一个线程。
1.2、线程调度
线程调度有两种方式: 分时调度,抢占式调度。
分时调度: 所有线程轮流获得 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度: 让线程自己抢占 CPU 的使用权,优先级高的线程抢占几率更大,如果优先级相同则会随机选择一个。
Java使用的就是抢占式调度。CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于 CPU 的一个核心而言,某个时刻, 只能执行一个线程,而 CPU 的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让 CPU 的使用率更高。
1.3、同步与异步
同步: 多个线程排队执行,效率低但是数据安全。
异步: 多个线程同时执行,效率高但是数据不安全。
1.4、并发与并行
并发: 两个或多个线程在同一时间段内执行。
并行: 两个或多个线程在同一时刻执行。对于多核 CPU 而言既是两个或多个线程在不同核心上的同一时刻抢到时间片执行。
2、多线程实现
Java 中要实现多线程有三种方式,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
2.1、Thread
想要实现多线程让一个类继承 Thread 类即可,则此类就是多线程实现类,且在该类中必须重写 Thread 类的 run 方法,该方法是线程的主体。最后调用继承自父类的 start 方法即可开启线程,其中对于一个多线程实现类只能调用一次 start 方法,多次调用会抛出异常。
类名: public class Thread extends Object implements Runnable
常用字段:
变量和类型 | 字段名 | 描述 |
---|---|---|
static int | MAX_PRIORITY | 线程可以拥有的最大优先级。 |
static int | MIN_PRIORITY | 线程可以拥有的最低优先级。 |
static int | NORM_PRIORITY | 分配给线程的默认优先级。 |
构造方法:
方法名 | 描述 |
---|---|
Thread() | 创建一个新的线程,使用默认的命名方式(Thread-n,其中n从0开始依次增加)。 |
Thread(Runnable target) | 为指定的任务target创建一个新的线程,使用默认的命名方式。 |
Thread(Runnable target, String name) | 为指定的任务target创建一个新的线程,使用name作为线程名称。 |
Thread(String name) | 创建一个新的线程,使用name作为线程名称。 |
常用方法:
变量和类型 | 方法名 | 描述 |
---|---|---|
static Thread | currentThread() | 返回当前正在执行的线程对象。 |
long | getId() | 返回次线程的标识符。 |
String | getName() | 返回此线程的名称。 |
int | getPriority() | 返回此线程的优先级。 |
void | interrupt() | 中断此线程。 |
void | run() | 在主线程中直接执行多线程实现类的run方法,并不会创建新的线程。 |
void | setDaemon(boolean on) | 将此线程标记为守护线程。 |
void | setName(String name) | 将此线程的名称修改为name。 |
void | setPriority(int newPriority) | 将此线程的优先级修改为newPriority。 |
static void | sleep(long millis) | 使当前正在执行的线程休眠指定的毫秒数mills。 |
void | start() | 创建一个新的线程并开始执行,由Java虚拟机调用此线程的run方法。 |
运行一段代码时会启动 main 方法,而 main 方法在启动时也会创建一个线程,该线程是程序的主线程,一个线程中创建的线程的优先级默认是等同于创建它的线程。一般创建的线程默认都是用户线程,只有守护线程中创建的线程为守护线程。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadTest
*@Description: Thread类的使用
*
*/
public class ThreadTest {
static class MyThread extends Thread {
// 使用父类即Thread的一参构造方法创建一个名为name的子线程
public MyThread(String name) {
super(name);
}
// 使用父类的无参构造方法创建一个默认的子线程
public MyThread() {
}
@Override
public void run() {
// Thread thread = this;
// 使用静态方法currentThread获得当前线程对象,在run方法中获得对象相当于this,即上行代码作用等于与下行代码
Thread thread = Thread.currentThread();
// 使用getName获取线程名称,使用getId获取线程标识,使用getPriority获取线程优先级
System.out.println(thread.getName() + "子线程已启动,线程标记为:" + thread.getId() + ",线程优先级为:" + thread.getPriority());
}
}
public static void main(String[] args) throws InterruptedException {
// 创建一个默认子线程
MyThread myThread1 = new MyThread();
// 创建一个名为aaa的子线程
MyThread myThread2 = new MyThread("aaa");
// 启动第一个子线程
myThread1.start();
// 启动第二个子线程
myThread2.start();
// 在main方法中使用currentThread获得的就是主线程
Thread thread = Thread.currentThread();
System.out.println("主线程名称:" + thread.getName() + ",主线程标记:" + thread.getId() + ",主线程优先级:" + thread.getPriority());
// 一个线程实例只能启动一次,多次启动会报错
System.out.println("再一次启动第一个子线程:");
myThread1.start();
// 多次执行程序会发现程序的执行结果不一样,原因是多线程程序并不会顺序执行,哪个线程抢到了CPU资源则哪个线程就先执行
}
}
2.2、Runnable
接口定义: public interface Runnable
使用 Runnable 实现多线程步骤:
- 创建 Runnable 的实现类。
- 在实现类中重写 run 方法。
- 创建实现类对象
- 以实现类对象为参数构造 Thread 类实例
- 使用 Thread 类实例调用 start 方法启动线程
Runnable 接口中只有一个抽象方法 run,所以它的实现类不包含任何 Thread 类方法,如想要修改线程需要在线程中使用 Thread 类的静态方法获取线程对象。
使用 Runnable 接口实现多线程为什么需要使用 Thread 类的 start 方法?
- 实现多线程需要本机操作系统的支持,而 Runnable 接口只有一个抽象方法,其自身无法实现多线程,需要借助 Thread 类。
- Thread 类中的 start 方法中调用了 start0 方法,此方法使用了 native 关键词,此关键词表示调用本机操作系统函数。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: RunnableTest
*@Description: Runnable的使用
*
*/
public class RunnableTest {
static class MyRunnale implements Runnable {
@Override
public void run() {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
System.out.println(i + " --> " + thread.getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 创建一个Runnable接口的实现类对象,表示创建的一个任务
MyRunnale myRunnale = new MyRunnale();
// 以实现类对象为参数创建一个Thread类对象,表示创建一个线程并为其指派了一个任务
Thread thread = new Thread(myRunnale);
// 启动这个线程
thread.start();
// 主线程中的操作
Thread main = Thread.currentThread();
for (int i = 0; i < 5; i++) {
System.out.println(i + " --> " + main.getName());
try {
// 每一次打印后线程休眠1000毫秒,直观结果每一秒打印一次
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.3、Callable
接口定义: public interface Callable<V>
主线程中创建的线程的执行不会影响到主线程,而使用 Callable 创建的线程会有一个返回值,主线程在创建该线程后会一直等待接收该返回值,然后主线程程序才会继续执行。
接口中只包含一个抽象方法 V call() throws Exception
其中 call 为线程主体,V 为泛型表示自定义返回值类型。
Callable 使用步骤:
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
new Thread(future).start();
FutureTask:
类名: public class FutureTask<V> extends Object implements RunnableFuture<V>
常用方法:
变量和类型 | 方法名 | 描述 |
---|---|---|
V | get() | 返回一个结果V,使用此方法的线程会一直阻塞直到接收这个结果。 |
V | get(long timeout, TimeUnit unit) | 返回一个结果V,使用此方法的线程会在timeout的时间内阻塞直到接收结果,超出时间则抛出异常,unit用于指定时间的单位(毫秒、秒、分钟等)。 |
boolean | isDone() | 判断此线程是否已经结束,结束则返回true,否则返回false。 |
boolean | cancel(boolean mayInterruptIfRunning) | 中断此线程,mayInterruptIfRunning为true则中断,false则不中断。 |
boolean | isCancelled() | 判断此线程是否被中断,是中断的则返回true,不是则返回false。 |
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeoutException;
/**
*@ClassName: CallableTest
*@Description: Callable的使用
*
*/
public class CallableTest {
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程休眠2秒后再返回结果
Thread.sleep(2000);
return "子线程结果已返回!";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建Callable线程并指派一个任务
FutureTask task = new FutureTask(new MyCallable());
// 使用匿名对象启动线程执行任务
new Thread(task).start();
for (int i = 1; i < 6; i++) {
System.out.println(i + "次循环");
if (i == 3) {
System.out.println(i + "次循环,等待子线程返回值:");
// 第3次循环时等待子线程的返回结果并打印
String result = (String) task.get();
System.out.println(result);
}
}
}
}
2.4、Thread、Runnable、Callable的区别
Thread 与 Runnable:
区别 | Thread | Callable |
---|---|---|
实现方式 | 继承Thread类 | 实现Callable接口 |
线程启动方式 | 使用继承自父类的start方法 | 以实现类为参数创建Thread类实例并使用start方法 |
线程池操作 | 不支持 | 支持 |
优点 | 1. 可直接使用Thread类方法 | 1. 适合多个线程同时执行相同的任务,实现资源共享 2. 避免单继承带来的局限性 3. 任务与线程分离,提高程序的健壮性 |
缺点 | 1. 单继承局限,已有父类则不能是用Thread 2. 多个相同线程无法资源共享 | 1. 不能直接使用Thread类方法 |
Runnable 与 Callable:
相同点:
- 都是接口。
- 都可以编写多线程程序。
- 都采用 Thread.start() 的方式启动线程
不同点:
- Runnable 没有返回值;Callable 可以返回执行结果 。
- Callable 接口的 call() 允许抛出异常;Runnable 的 run() 不能抛出 。
3、线程的状态
线程具有6种状态:创建NEW、运行RUNNABLE、阻塞BLOCKED、等待WAITING,计时等待TIMED_WAITING,终止TERMINATED。
NEW:
使用构造方法创建了一个 Thread 类对象后,新的线程对象便处于创建状态,此时线程并没有启动。
RUNNABLE:
使用线程的 start 方法后,线程就被启动,它可能正在 Java 虚拟机上执行,也可能没有抢到时间片正在等待系统的资源。当线程中有IO操作需要等待用户输入时,就操作系统层面而言,CPU不在执行此线程,即线程阻塞直到阻塞原因消除(IO操作完成),但是在JVM虚拟机层面上,线程仍处于 RUNNABLE 状态,就如同 RUNNABLE 状态下包含了传统的(操作系统层面) ready 和 running 即就绪和运行状态一样,IO 操作时的线程阻塞也在其中。
BLOCKED:
处于阻塞状态的线程,即一个线程正在等待一个监视器的锁释放以进入到同步代码块或同步方法中。
WAITING:
等待状态的线程,当一个线程调用了无参的 wait() 或 无参的 join() 方法时将处于等待状态,此时线程将停止执行,只有另一个拥有监视器对象锁的线程调用 notify() 或 notifyAll() 方法才能将其唤醒,或者是处于 WAITING 的线程等待调用了 join() 方法的线程终止,此线程将自动唤醒。
TIMED_WAITING:
出于计时等待的线程,当一个线程调用了 sleep() 或 有参的 wait(long) 或 有参的 join(long) 方法时将处于计时等待状态,线程将停止执行,直到等待时间超时后将自动唤醒继续执行。
TERMINATED:
线程终止状态,也可称死亡状态,线程执行结束或为线程添加中断标记且捕获到中断异常并人为停止线程的继续执行时线程将进入终止状态。终止的线程将无法再执行。
线程死亡使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadInterruptTest
*@Description: 线程中断使用案例
*
*/
public class ThreadInterruptTest {
public static void main(String[] args) {
// 使用匿名类直接实现Thread的子类
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 7; i++) {
System.out.println("子线程 --> " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 当校测到中断标记时进入catch块
System.out.println("发现中断标记。即将人为使子线程死亡!");
// 直接return表示人为停止该线程,线程将死亡
return;
}
}
}
};
// 启动线程
thread.start();
for (int i = 0; i < 7; i++) {
// 当i等于3时使用interrupt方法为子线程添加中断标记
if (i == 3) {
thread.interrupt();
}
System.out.println("主线程 --> " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4、守护线程和用户线程
线程分为两种:守护线程和用户线程。
用户线程: 当一个进程不包含任何存活的用户线程时,进程结束。
守护线程: 守护用户线程,当最后一个用户线程死亡时,所有的守护线程自动死亡。
一般而言创建的所有线程默认是用户线程,添加了守护标记的线程将转变为守护线程,守护线程创建的线程是守护线程。
使用 Thread 类下的 setDaemon 方法可以将一个线程设置为守护线程,其中需要传入参数 true 才可以。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadDaemonTest
*@Description: 守护线程使用案例
*
*/
public class ThreadDaemonTest {
public static void main(String[] args) {
// 主线程创建的匿名子线程1
Thread thread = new Thread() {
@Override
public void run() {
// 子线程1在线程中又创建了匿名子线程2
Thread thread1 = new Thread() {
@Override
public void run() {
// 子线程2每隔一秒打印0-9
for (int i = 0; i < 10; i++) {
System.out.println("子线程2 --> " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 在子线程1中启动子线程2
thread1.start();
// 子线程1每隔一秒打印0-9
for (int i = 0; i < 10; i++) {
System.out.println("子线程1 --> " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 将子线程1设置为守护线程,则子线程1创建的子线程2将自动转变为守护线程
thread.setDaemon(true);
// 启动子线程1
thread.start();
// 主线程中打印0-4
for (int i = 0; i < 5; i++) {
System.out.println("主线程 -----> " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 由于主线程打印次数要少,所以主线程可能先结束,主线程打印结束后守护线程1和2都将暂停打印
// 主线程打印完4并不是马上就死亡,它死亡需要时间,而这期间守护线程1和2可能还可以打印几次
}
}
5、同步和死锁
5.1、线程不安全
使用多线程时最常见的问题就是线程不安全问题。
线程不安全:多个线程对同一数据进行操作时,由于某一个线程对数据进行了修改可能导致其他正在同时执行的线程发生错误。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadSafeTest
*@Description: 线程安全问题案例
*
*/
public class ThreadSafeTest {
static class Ticket implements Runnable {
private int ticket = 10; //买票,一共有10张票
@Override
public void run() {
Thread thread = Thread.currentThread();
// 当还有余票则一直循环买票
while (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖出一张票
ticket--;
System.out.println(thread.getName() + "卖出一张票...余票:" + ticket);
}
}
}
public static void main(String[] args) {
// 创建任务
Ticket ticket = new Ticket();
// 创建线程并指派任务
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
// 启动线程
thread1.start();
thread2.start();
thread3.start();
/**
* 结果说明:这只是一种可能,仅供参考
* 当ticket=1时
* 子线程1抢到时间片开始执行,条件满足进入了while循环,由于休眠导致线程停止执行进入计时等待状态
* 子线程1停止买票,此时ticket=1
* 子线程2抢到时间片开始执行,条件满足进入了while循环,由于休眠导致线程停止执行进入计时等待状态
* 子线程2停止买票,此时ticket=1
* 子线程0抢到时间片开始执行,条件满足进入了while循环,由于休眠导致线程停止执行进入计时等待状态
* 子线程0停止买票,此时ticket=1
* 子线程1休眠结束且抢到时间片继续执行
* 子线程1卖出一张票,此时ticket=0,不满足循环条件,执行完毕,子线程1死亡
* 子线程2休眠结束且抢到时间片继续执行
* 子线程2卖出一张票,此时ticket=-1,不满足循环条件,执行完毕,子线程2死亡
* 子线程0休眠结束且抢到时间片继续执行
* 子线程0卖出一张票,此时ticket=-2,不满足循环条件,执行完毕,子线程0死亡
* 三个子用户线程死亡,主线程执行完毕死亡,程序执行结束
*/
}
}
5.2、同步
解决线程不安全问题可以使用同步,使一个线程在使用共享资源时不允许其他线程访问共享资源,直到该线程对共享资源的使用完毕。即同一时刻只允许一个线程访问共享资源进行操作。
使用同步有三种方式:同步代码块、同步方法、显式锁。
5.2.1、同步代码块
使用 synchronize 关键字括起来的代码块就是同步代码块,使用时需要传入一个锁对象,当一个线程取得锁对象的锁时便可执行同步代码块,其他试图执行同步代码块却没有锁对象的锁的线程将被阻塞。对象锁将在同步代码块执行完毕时释放。锁对象可以是任意对象,但一定要保证它是一个所有线程都能找到的对象,例如多线程实现类的成员变量,这是所有线程共享的。run 方法中的变量不可取,它的有效范围仅在一个线程的方法中。一般可以使用 this 即当前对象来作为锁对象,因为线程实现类对象表示任务,多个线程执行同一个任务时任务对象是共享的。
使用方式:
synchronized(锁对象){
需要同步的代码;
}
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadSynchronizeTest1
*@Description: 同步代码块的使用
*
*/
public class ThreadSynchronizeTest1 {
static class Ticket implements Runnable {
private int ticket = 10; //买票,一共有10张票
@Override
public void run() {
Thread thread = Thread.currentThread();
// 一直循环买票
while (true) {
// 使用当前任务对象做锁对象,锁存在时第一个执行到这里的线程将获得对象锁,没有锁的线程执行到这里将被阻塞
synchronized (this) {
// 只有还有余票时才出一张票,否则退出循环
if (ticket > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖出一张票
ticket--;
System.out.println(thread.getName() + "卖出一张票...余票:" + ticket);
} else
break;
}
// 同步代码块执行完毕,该线程释放锁
}
}
}
public static void main(String[] args) {
// 创建任务
Ticket ticket = new Ticket();
// 创建线程并指派任务
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
// 启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
5.2.2、同步方法
使用 synchronize 修饰的方法就是同步方法,事实上使用同步方法时会将 this 即拥有该方法的类对象作为锁对象,然后与同步代码块类似,只有拥有对象锁的线程才能调用方法,否则调用时会被阻塞,方法结束后释放锁。
注意: 同步方法实际默认使用 this 作为锁对象,当有多个同步方法时,且有一个或多个同步代码块都使用 this 作为锁对象时,一旦有一个线程获得了对象锁,那么其他线程将无法调用任何同步方法且无法执行任何以 this 为锁对象的同步代码块。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ThreadSynchronizeTest2
*@Description: 同步方法的使用
*
*/
public class ThreadSynchronizeTest2 {
static class Ticket implements Runnable {
private int ticket = 10; //买票,一共有10张票
@Override
public void run() {
Thread thread = Thread.currentThread();
// 一直循环买票
// 同步方法会使用当前任务对象this做锁对象,锁存在时第一个使用该方法的线程将获得对象锁,没有锁的线程调用方法将被阻塞
while (sale(thread)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized boolean sale(Thread thread) {
// 只有还有余票时才出一张票,否则退出买票
if (ticket > 0) {
// 卖出一张票
ticket--;
System.out.println(thread.getName() + "卖出一张票...余票:" + ticket);
// 返回true继续买票
return true;
} else
// 返回false不在退出买票
return false;
// 执行完毕,该线程释放对象锁
}
}
public static void main(String[] args) {
// 创建任务
Ticket ticket = new Ticket();
// 创建线程并指派任务
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
// 启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
5.2.3、显式锁Lock
使用 Lock 人为的创建锁对象,手动上锁并解锁。
同步代码块和同步方法都是用了 synchronize 关键字,它实际使用的就是隐式锁,我们不需要关心如何上锁和释放锁,由程序自动完成。
显式锁与隐式锁的区别:
区别 | 隐式锁synchronize | 显式锁Lock |
---|---|---|
构成 | Java关键字,由JVM来维护,JVM层面的锁 | 是一个具体的类,使用Lock调用对应的API,是API层面的锁 |
使用方式 | 程序自动获取锁和释放锁 | 手动添加代码去获取锁和释放锁 |
中断 | 不可中断,除非抛出异常或执行完毕 | 可以中断,使用API中的方法可实现中断 |
公平 | 一定是非公平锁 | 默认是非公平锁,也可以通过参数设置为公平锁 |
性能 | 使用悲观锁机制,线程独占锁,其他线程会被阻塞,导致线程上下文切换,效率较低 | 使用乐观锁机制,假设没有冲突进行操作,当进行数据提交和更新时检测到冲突则操作失败,一直尝试直到成功为止,是非阻塞式的,效率较高 |
公平锁: 锁被锁住时其他等待锁释放的线程会排队等待,先到的先获取释放后的锁。
非公平锁: 锁被释放时,所有等待的线程一起抢占锁,那个线程抢到那个线程获得锁。
Lock 在创建对象时传入参数 fair 的值,值为 true 表示公平锁,值为 false 表示非公平锁。
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*@ClassName: ThreadLockTest
*@Description: 显式锁Lock的使用
*
*/
public class ThreadLockTest {
static class Ticket implements Runnable {
private int ticket = 10; //买票,一共有10张票
// 创建锁对象,该锁为显式锁,无参构造默认创建非公平锁
private Lock lock = new ReentrantLock();
// 下行代码创建的就是公平锁
//private Lock lock = new ReentrantLock(true);
@Override
public void run() {
Thread thread = Thread.currentThread();
// 一直循环买票
while (true) {
// 手动上锁
lock.lock();
// 只有还有余票时才出一张票,否则退出循环
try {
if (ticket > 0) {
// 卖出一张票
ticket--;
System.out.println(thread.getName() + "卖出一张票...余票:" + ticket);
} else {
break;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 手动解锁
lock.unlock();
}
}
}
}
public static void main(String[] args) {
// 创建任务
Ticket ticket = new Ticket();
// 创建线程并指派任务
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
// 启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
5.3、死锁
在使用多线程共享资源时为了保证资源的完整性会使用同步,而更多地同步就有可能造成死锁,即两个线程相互等待对方先完成,造成程序的停滞。在开发中,我们只能尽量去避免死锁的产生,在调用一个会产生锁的方法时要避免再去调用另一个能产生锁的方法。
死锁案例:
package work.java.xzk10301008;
/**
*@ClassName: DeadlockTest
*@Description: 死锁案例
*
*/
public class DeadlockTest {
static class Person1 {
public synchronized void say(Person2 lisi) {
System.out.println("张三说:李四你还我钱,我就把欠条给你。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lisi.fun();
}
public synchronized void fun() {
System.out.println("张三给了李四欠条,李四还了张三钱。");
}
}
static class Person2 {
public synchronized void say(Person1 zhangsan) {
System.out.println("李四说:张三你给我欠条,我就把钱还给你。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
zhangsan.fun();
}
public synchronized void fun() {
System.out.println("李四还了张三钱,张三给了李四欠条。");
}
}
static class MyThread implements Runnable {
private Person1 zhangsan;
private Person2 lisi;
public MyThread(Person1 p1, Person2 p2) {
this.zhangsan = p1;
this.lisi = p2;
}
@Override
public void run() {
// lisi的say方法会产生锁,而方法里面又调用了会产生锁的zhangsan的fun方法
lisi.say(zhangsan);
}
}
public static void main(String[] args) {
Person1 zhangsan = new Person1();
Person2 lisi = new Person2();
Thread thread = new Thread(new MyThread(zhangsan, lisi));
thread.start();
// zhangsan的say方法会产生锁,而里面又调用了会产生锁的lisi的fun方法
zhangsan.say(lisi);
}
}
5.4、生产者与消费者问题
生产者与消费者问题是多线程中的经典案例。标准的流程是生产者生产一个数据对象,消费者拿走一个数据对象,如此先后循环,实际的使用时会产生两个问题:
- 生产者生产数据对象到一半时,消费者过来拿走了数据对象,导致数据对象掺杂了新的数据与上一次的数据。
- 生产者连续生产了多个数据对象消费者才过来拿走一个,或消费者连续拿走了同一个对象而生产者还没来得及生产新的对象。
解决问题1的方式是使用同步,当生产者生产数据对象时消费者无法拿走对象。
解决问题2的方式是使两个线程进行通信,当生产者线程产出一个数据对象后停下生产工作并通知消费者过来拿取数据对象,消费者拿走了数据对象后停下自己的继续拿取的操作并通知生产者可以继续生产。
Object类对线程通信的支持:
变量和类型 | 方法名 | 描述 |
---|---|---|
void | notify() | 随机唤醒一个在此对象的监视器上等待的线程 |
void | notifyAll() | 唤醒所有在此对象的监视器上等待的线程 |
void | wait() | 使当前线程休眠一直等待,直到被唤醒 |
void | wait(long timeoutMillis) | 使当前线程休眠一直等待,直到被唤醒,或者在指定时间后自己醒来 |
注意: 以上方法只能在同步代码块或同步方法中调用,调用了 wait 方法的线程将会自动释放对象监视器的锁。调用了 notify 或 notifyAll的线程只有在执行完同步代码释放锁后,被唤醒的线程才能开始执行。
使用案例:
package work.java.xzk10301008;
/**
*@ClassName: ProducesconsumableTest
*@Description: 生产者与消费者问题案例
*
*/
public class ProducesconsumableTest {
// 食物类
static class Food {
private String name;
private String taste;
// 操作标记,true时厨师可以做菜,服务员不能端菜;false时厨师不能做菜,服务员可以端菜
private boolean flag = true;
// 厨师做菜
public synchronized void cooking(String name, String taste) {
if (flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
System.out.println("厨师做出了一道" + taste + name);
// 修改标记
flag = false;
// 唤醒服务员线程去端菜
notifyAll();
try {
// 休眠厨师线程停止做菜
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 服务员端菜
public synchronized void takeFood(int i) {
if (!flag) {
System.out.println("服务员拿走了一道" + taste + name);
// 修改标记
flag = true;
// 唤醒厨师线程继续做菜
this.notifyAll();
try {
// 当端到最后一道菜时不用在端菜了,不用继续等待了,线程将执行完毕
if (i != 5)
// 休眠服务员线程停止端菜
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Cook extends Thread {
private Food food;
public Cook(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 6; i++) {
if (i / 2 == 0) {
food.cooking("红烧肉", "香辣味");
} else {
food.cooking("鱼香肉丝", "甜辣味");
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Waiter extends Thread {
private Food food;
public Waiter(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 6; i++) {
food.takeFood(i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return;
}
}
public static void main(String[] args) throws InterruptedException {
Food food = new Food();
new Cook(food).start();
new Waiter(food).start();
}
}
6、线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
使用线程池的好处:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
Java 中主要有四种线程池:缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池。
6.1、缓存线程池
缓存线程池的长度没有限制,它的使用流程是:
- 判断线程池是否存在空闲的线程
- 存在则使用其执行任务
- 不存在则创建一个线程并将其放入线程池中,然后执行任务
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*@ClassName: ExecutorService1Test
*@Description: 缓存线程池的使用
*
*/
public class ExecutorService1Test {
public static void main(String[] args) {
// 获取缓存线程池
ExecutorService service = Executors.newCachedThreadPool();
// 使用缓存线程池执行一个新的任务,这里使用了Runnable的匿名内部类
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
}
});
// 主线程休眠1秒
try {
Thread.sleep(2000);
System.out.println("主线程休眠两秒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 观察执行新任务的线程
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
}
});
service.shutdown();
}
}
6.2、定长线程池
定长线程池的长度是固定的,它的使用流程是:
- 判断线程池中是否存在空闲的线程
- 存在则使用其执行任务
- 不存在空闲线程且线程池未满的情况下,创建新线程并放入线程池,然后执行任务
- 不存在空闲线程且线程池已满的情况下,等待线程池出现空闲线程就执行任务
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*@ClassName: ExecutorService2Test
*@Description: 定长线程池的使用
*
*/
public class ExecutorService2Test {
public static void main(String[] args) {
// 获取定长线程池并制定长度2
ExecutorService service = Executors.newFixedThreadPool(2);
// 使用定长线程池执行一个新的任务,这里使用了Runnable的匿名内部类
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
// 使线程休眠三秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
// 使线程休眠三秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 执行新任务,会等待线程池出现空闲线程才执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
}
});
// 关闭线程池
service.shutdown();
}
}
6.3、单线程线程池
单线程线程池长度为 1,效果与定长线程池创建时传入参数 1 一样,使用流程是:
- 判断线程池中的那个线程是否空闲
- 该线程空闲则执行任务
- 该线程不空闲则等待,当线程空闲后执行任务
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*@ClassName: ExecutorService3Test
*@Description: 单线程线程池的使用
*
*/
public class ExecutorService3Test {
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
// 使线程休眠两秒
try {
System.out.println(Thread.currentThread().getName() + "休眠两秒...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 获取单线程线程池
ExecutorService service = Executors.newSingleThreadExecutor();
// 创建任务
MyRunnable runnable = new MyRunnable();
// 多次执行任务,观察执行任务的线程
service.execute(runnable);
service.execute(runnable);
service.execute(runnable);
// 关闭线程池
service.shutdown();
}
}
6.4、周期性任务定长线程池
周期性任务定长线程池的长度是固定的,它的使用流程是:
- 判断线程池中是否存在空闲的线程
- 存在则使用其执行任务
- 不存在空闲线程且线程池未满的情况下,创建新线程并放入线程池,然后执行任务
- 不存在空闲线程且线程池已满的情况下,等待线程池出现空闲线程就执行任务
流程与定长线程池一样,但当执行周期性任务时,它会定时执行,在某个时机触发,自动执行任务。
定时执行传入参数:
- Runnable 类型任务
- 时长数字(该时间后执行)
- 时长数字的单位(TImeUnit的常量指定)
周期执行传入参数:
- Runnable 类型任务
- 延迟时长数字(该时间后执行)
- 周期时长数字(没隔多久执行一次)
- 时长数字的单位(TImeUnit的常量指定)
使用案例:
package work.java.xzk10301008;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
*@ClassName: ExecutorService4Test
*@Description: 周期性任务定长程线程池的使用
*
*/
public class ExecutorService4Test {
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行...");
}
}
public static void main(String[] args) {
// 获取周期性任务定长线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
// 创建任务
MyRunnable runnable = new MyRunnable();
// 执行定时任务,3秒后执行
System.out.println("执行定时任务,3秒后执行:");
service.schedule(runnable, 3, TimeUnit.SECONDS);
for (int i = 1; i < 4; i++) {
System.out.println(i + "...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行周期任务,3秒开始执行,执行后每隔2秒周期执行
System.out.println("执行周期任务,3秒开始执行,执行后每隔2秒周期执行:");
service.scheduleAtFixedRate(runnable, 3, 2, TimeUnit.SECONDS);
for (int i = 1; i < 20; i++) {
System.out.println(i + "...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 关闭线程池
service.shutdown();
}
}
7、Lambda表达式
Lambda 表达式是 Java SE 8 引入的新特性,它使用函数式的编程思想,无需创建对象,不关注过程,只注重结果。有些时候使用它可以极大的减少冗余代码。
Lambda 表达式语法:(parameters) -> expression
或 (parameters) ->{ statements; }
Lambda 表达式的特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体只包含一条语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明返回值的表达式。
使用 Lambda 表达式可以快速的创建接口的实现类对象并重写抽象方法,但是,接口必须只包含一个抽象方法。
使用 Lambda 实现 Runnable 使用案例:
package work.java.xzk10301008;
/**
*@ClassName: RunnableLambdaTest
*@Description: 使用Lambda表达式实现Runnable
*
*/
public class RunnableLambdaTest {
public static void main(String[] args) {
// 使用匿名内部类实现继承Thread类并重写run方法
new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " -> Thread实现");
}
}.start();
// 使用匿名内部类实现Runnable接口并重写run方法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " -> Runnable实现");
}
}).start();
// 使用Lambda表达式实现Runnable的run方法
new Thread(() -> System.out.println(Thread.currentThread().getName() + " -> Lambda实现")).start();
}
}
使用Lambda实现自定义接口案例:
package work.java.xzk10301008;
import java.util.Map;
/**
*@ClassName: LambdaTest
*@Description: Lambda表达式的使用
*
*/
public class LambdaTest {
// 定义接口,只有一个抽象方法,表示操作
interface MyOperation {
int operate(int a, int b);
}
// 定义方法,传入操作数据a和b以及接口实现类
public static int operate(int a, int b, MyOperation myOperation) {
// 格式化字符串打印,使用占位符获取进行计算的参数以及计算操作对应的操作符
System.out.printf("%d %s %d = ", a, map.get(myOperation), b);
return myOperation.operate(a, b);
}
// 使用Lambda表达式直接实现接口以及接口方法并返回实例
static MyOperation inc = (a, b) -> a + b; //两数相加
static MyOperation sub = (a, b) -> a - b; //两数相减
static MyOperation mul = (a, b) -> a * b; //两数相乘
static MyOperation div = (a, b) -> a / b; //两数相除
static MyOperation max = (a, b) -> a > b ? a : b; //两数取较大值
// 使用JDK9集合新特性,直接创建固定大小集合,使具体的操作类与操作符合相互映射
static Map<MyOperation, String> map = Map.of(inc, "+", sub, "-", mul, "*", div, "/", max, "max");
public static void main(String[] args) {
// 调用方法,传入操作参数和操作类
System.out.println(operate(4, 2, inc));
System.out.println(operate(4, 2, sub));
System.out.println(operate(4, 2, mul));
System.out.println(operate(4, 2, div));
System.out.println(operate(4, 2, max));
}
}