4. 多线程
4.1 基本概念:程序、进程和线程
程序、进程和线程
程序:为了完成特定的任务,用某种语言编写的一组指令的集合。程序是一段静态的代码,静态对象。
进程:是程序的一次执行过程或正在运行的程序。(进程是一个任务)。进程是一个动态的过程:有产生、存在和消亡的过程——即拥有生命周期。
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存空间
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。一个进程可以包含一个或多个线程。
- 若一个进程同一时间执行多个线程,就是支持多线程。
- 线程作为执行和调度的单位,每个线程拥有独立的运行栈和程序计数器(pc, program counter register),线程切换开销比较小。
- 一个进程中的多个线程可以共享相同的内存单元/内存地址空间(它们从同一堆中分配对象,方法区、堆),可以访问相同的变量和对象。使得线程间通信更高效、便捷。但是多个线程操作共享的系统资源可能会带来安全隐患。
并行与并发
- 并行:多个CPU同时执行多个任务。
- 并发:一个CPU(采用时间片)同时执行多个任务。如:秒杀。
多线程的优点
- 提高程序的响应
- 提高CPU的利用率
- 改善程序结构
何时需要多线程
- 程序需要同时执行两个或多个任务
- 程序需要实现等待的任务
- 需要后台运行的程序
多进程和多线程比较
和多线程相比,多进程缺点:
- 创建进程比创建线程开销大
- 进程间通信比线程间通信慢,因为线程间通信是读写同一个变量,速度很快
多进程优点:
- 多进程稳定性比多线程高,以为多进程下,一个进程崩溃不会影响其他进程。而在多线程下,任何一个线程的崩溃会直接导致整个进程崩溃
Java语言内置了多线程支持:一个Java应用程序实际上是一个JVM进程,JVM进程用一个主线程执行main()
方法,在main()
内部,又可以启动多个线程。(一个java.exe,至少有三个线程:main()
主线程,gc()
垃圾回收线程,异常处理线程。)
线程的分类
Java线程分为两类:一种是守护线程,一种是用户线程。
- 在各当面几乎是一样的,唯一区别是判断JVM何时离开。
- 当用户线程执行结束,守护线程也会结束。
- 守护线程用来服务用户线程的,在
start()
前调用thread.setDaemon(true)
可以把一个用户线程变为守护线程 - Java垃圾回收机制就是一个典型的守护线程
- 若JVM中都是守护线程,当前JVM将退出
4.2 线程的创建和使用
创建多线程有四种方法,这里有两种,后续java 1.5增加了两种新方法。
Java语言的JVM允许程序运行多个线程,通过 java.lang.Thread
类来体现。
创建一个新线程有两种方法:
第一种:继承Thread 的方式
将一个类声明为 Thread 的子类,子类应重写Thread类的run方法,然后分配并启动子类的实例。
创建线程步骤:
- 创建一个继承于 Thread 类的子类
- 重写 Thread 类的 run() 方法
- 创建 Thread 类的子类的对象
- 通过对象调用 start() 方法
示例:
// 1. 继承于Thread
class MyThread extends Thread{
// 2. 重新run方法
@Override
public void run() {
System.out.println("child thread name: "+getName()); // 也可以用 Thread.currentThread().getName()
// 业务写在这个方法中
}
}
public class TheadTest {
public static void main(String[] args) {
// 3. 创建对象
MyThread myThread = new MyThread();
myThread.setName("子线程"); // 设置线程名称
// 4. 通过对象调用 start 方法
myThread.start();
Thread.currentThread.setName("主线程"); // 设置主线程名称
System.out.println("main thread name: "+Thread.currentThread().getName());
}
}
输出结果:
main thread name: 主线程
child thread name: 子线程
如果要创建多个线程
// 如果创建多个子线程的话,需要
MyThread myThread = new MyThread();
myThread.start();
MyThread myThread2 = new MyThread();
myThread2.start();
MyThread myThread3 = new MyThread();
myThread3.start();
多个线程共享一个静态变量:
当多个线程都想要共用一个值时,比如卖票时的票数,可以将类变量设置为 staitc。
start() 方法的作用
- 启动当前线程
- Java虚拟机调用此线程的
run
方法
创建Thread 的匿名子类
简单一点的话,可以写Thread 的匿名子类
// 创建Thread 的匿名子类
new Thread(){
@Override
public void run() {
System.out.println("test2");
}
}.start();
使用Java8引入的 lambda
new Thread(()->{
System.out.println("t3");
}).start();
线程的常用方法
start()
启动当前线程;调用run方法run()
通常需要重写Thread类的此方法,将业务写在此方法中currentThread()
静态方法,返回执行当前代码的线程getName()
获取当前线程的名称setName()
设置当前线程的名称yield()
线程让步,释放当前CPU的执行权。(暂停当前正在执行的线程,给优先级相同或更高的线程让步)join()
当某个程序执行流中调用其他线程的join()
方法时,调用线程将被阻塞,直到被调用线程执行完为止。(在线程a中调用线程b的join(), 此时线程a进入阻塞状态,直到线程b执行完之后,线程a才结束阻塞状态,继续执行后续程序。)stop()
强制线程生命期结束,不推荐sleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)isAlive()
判断当前线程是否存活getPriority()
返回此线程的优先级setPriority(int newPriority)
更改线程的优先级
线程的调度
调度策略:
- 时间片
- 抢占式:高优先级的线程抢占CPU
Java的调度方法:
- 同优先级线程组成先进先出队列,使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级
- MAX_PRIORITY: 10 (最高优先级)
- MIN_PRIORITY: 1 (最低优先级)
- NORM_PRIORITY: 5 (普通优先级)
线程创建时继承父线程的优先级。
低优先及只是获得调用的概率低,并不意味着只有当高优先级的线程执行完以后才会执行低优先级。
第二种:实现 Runnable 接口的方式
第二种创建线程的方式是:创建一个实现 Runnable 接口的类,类实现了run() 方法,将类的对象传递给Thread() 并启动。
创建线程步骤:
- 创建一个实现了 Runnable 接口的类
- 实现类去实现 Runnable 的抽象方法 run()
- 创建实现类的对象
- 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 通过 Thread 类的对象调用 start() 方法
示例:
/**
* 多线程的创建,方式二,实现Runnable接口的方式
* @author chadJ
* @create 2022-03-04 18:49
*/
//1. 创建一个实现了 Runnable 接口的类
class MyThread2 implements Runnable{
//2. 实现类去实现 Runnable 的抽象方法 run()
@Override
public void run() {
System.out.println("t2");
}
}
public class ThreadRunnable {
public static void main(String[] args) {
//3. 创建实现类的对象
MyThread2 t2 = new MyThread2();
//4. 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
Thread thread = new Thread(t2);
//5. 通过 Thread 类的对象调用 start() 方法
thread.start();
}
}
如果要创建多个线程
// 如果创建多个子线程的话
// t2 对象可以直接使用,也能共享里面的变量,因为只有一个对象
MyThread2 t2 = new MyThread2();
Thread thread = new Thread(t2);
thread.start();
Thread thread2 = new Thread(t2);
thread2.start();
Thread thread3 = new Thread(t2);
thread3.start();
start()方法如何执行到run
这里,最后的 start() 方法,是怎么执行到 MyThread2 中的 run() 方法的呢。原来在
Thread.run中有如下代码,如果target 不为空,则执行 target 的run() 方法
public void run() {
if (target != null) {
target.run();
}
}
其中,target 是我们调用 Thread(t2) 时通过构造函数传过去的 Runnable
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
两种方式的比对
一种是继承Thread, 一种是实现Runnable 接口。
开发中,优先选择:实现 Runnable 接口的方式,
原因:
- 实现的方式没有类的单继承性的局限性,可以实现了Runnable接口后,实现其他接口
- 实现的方式更适合来处理多个线程有共享数据的情况(实现自然而然能共享数据,继承得给变量加 static)
联系:Thread 也实现的 Runnable。
相同点:两种方式都需要重写 run()
方法,将线程要执行的逻辑声明在 run()
中。
第三种:实现 Callable 接口
JDK5.0 新增两种线程创建方式:实现 Callable 接口、使用线程池 方式。
与使用 Runnable 相比,Callable 功能更强大:
-
相比
run()
方法,可以有返回值 -
方法可以抛出异常(之前的
run()
不可以抛出,因为原方法没抛出异常,重写的不能抛出) -
支持泛型的返回值
-
需要借助 FutureTask 类,比如获取返回结果
Futrue 接口
- 可以对具体Runnable Callable 任务的执行结果进行取消、查询是否完成、获取结果等操作。
- FutureTask 是 Future 接口的唯一实现类
- FutureTask 同时实现了 Runnable 和 Future接口。即可以作为Runnable 被线程执行,又可以作为 Future 得到Callable 的返回值。
创建线程步骤:
- 创建一个实现 Callable 的实现类
- 实现
call()
方法,将业务放在call()
中。可以有返回值,可以抛出异常 - 创建 Callable 接口实现类的对象
- 将此 Callable 实现类的对象作为参数传递 FutureTask 构造器中,创建 FutureTask 对象
- 将FutureTask 对象作为参数传递到 Thread类构造器中(Runnable多态,因为FutureTask实现了Runnable接口),创建 Thread 对象,并调用
start()
- 可以使用
get()
获取call()
中的返回值。(get()
返回值即为 FutureTask 构造器参数 Callable 实现类重写call()
方法的返回值。)
package com.acfuu.java;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程方式三:实现Callable接口
* @author chadJ
* @create 2022-03-06 14:11
*/
// 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++) {
sum+=i;
}
return sum;
}
}
public class CallableThread {
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();
try {
// 6. 可以使用 get() 获取call() 中的返回值。
// get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写 call()方法的返回值。
Object r = futureTask.get();
System.out.println("返回值:"+r);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
第四种:使用线程池(常用)
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁,实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用池中线程,不用每次都创建)
- 便于线程管理
- corePoolSize: 核心池大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时的存活时间
线程池相关 API
JDK5.0起提供了线程池API:ExecutorService 和 Executors
ExexutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行 Runnable
:执行任务,有返回值,一般用来执行 CallableFuture submit(Callable task) void shutdown()
:关闭连接池
Executors: 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool()
:创建一个可重用固定线程池数的线程池(常用)Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:在给定延迟后创建一个线程池
线程创建步骤:
- 提供指定线程数量的线程池
- 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
- 关闭连接池
示例:
package com.acfuu.java;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 创建线程方式四:使用线程池
* @author chadJ
* @create 2022-03-06 14:47
*/
class NumThread2 implements Runnable{
private boolean odd = true;
public NumThread2(boolean odd) {
this.odd = odd;
}
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
if(odd){
if(i%2==1) System.out.println(Thread.currentThread().getName()+":"+i);
} else{
if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 设置线程池属性
// System.out.println(executorService.getClass());
// ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
// executor.setCorePoolSize(1); // 设置为1后,线程池中就不是10个线程,就只有一个线程了
// 2. 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
executorService.execute(new NumThread2(true)); // 适用于 Runnable
executorService.execute(new NumThread2(false));
//executorService.submit(xxx); // 适用于 Callable
// 3. 关闭连接池
executorService.shutdown();
}
}
输出:
pool-1-thread-1:1
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
pool-1-thread-2:10
pool-1-thread-1:3
pool-1-thread-1:5
pool-1-thread-1:7
pool-1-thread-1:9
4.3 线程的生命周期
JDK 中的Thread.State 类定义了线程的状态。一个线程对象只能调用一次 start() 方法启动新线程(所以不能一个Thread对象多次start),并在新线程中执行 run()
方法。一旦 run()
执行完毕,线程就结束了。
Java 线程的状态有:
- New, 新创建的线程,尚未运行
- Runnable, 运行中的线程,正在执行
run()
方法的代码 - Blocked, 运行中的线程,因为某些操作被阻塞而挂起
- Wating, 运行中的线程,因为某些操作在等待
- Time Waiting, 运行中的线程,因为执行
sleep()
方法正在计时等待 - Terminated, 线程已终止,
run()
执行完毕
线程声明周期:
4.4 线程的同步
线程的同步是为了解决线程的安全问题。
4.4 线程的同步
线程的同步是为了解决线程的安全问题。
问题:比如卖票出现重票、错票,这就叫出现了线程的安全问题。
解决:当一个线程a在操作的时候,其他线程不能参与进来,直到线程a操作结束其他线程才可以开始操作。即使线程a出现了阻塞,也不能执行其他线程。
线程的安全问题
在多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。通过加锁和解锁操作,就能保证指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期间被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区,任何时候临界区最多只有一个线程能执行。
在Java中,通过同步机制,解决线程的安全问题。
同步机制解决线程安全问题
使用synchronized (发音 /'sɪŋkrənaɪzd/),有两种方法:
- 同步代码块
synchronized(lock) { // lock锁也叫同步监视器
// 需要被同步的代码。(操作共享数据的代码)
}
任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。
在实现Runnable 接口创建多线程的方式中,可以考虑使用 this 充当 lock。
在继承Thread 类创建多线程的方式中,(慎用this充当lock),考虑使用当前类充当lock。(比如 MyThread.class)
-
同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
需要同步的方法使用 synchronized 修饰:
1.在实现 Runnable 接口的方式中:
此种方法中,lock 是 this。
public synchronized void test(){ // 在test中,同步监视器/锁就是 this
...
}
public void run(){
...
test();
...
}
`run()` 方法也可以使用 synchronized修饰, `sysnchronized void run()`,这种要确保 `run()`中使完整的同步数据。不然包裹的太多,效率会低。
2.在继承 Thread 类的方式中:
为了保证正常执行,需要多加个 static 。
此种方法中,lock 是 当前类。
public static synchronized void test2(){ // 在test2中,同步监视器/锁就是 当前类
...
}
public void run(){
...
test2();
...
}
总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
- 非静态的同步方法,同步监视器是 *this*
- 静态的同步方法,同步监视器是 *当前类本身*。
线程同步优缺点
- 同步的方式,解决了线程的安全问题。
- 操作同步代码时,只能有一个线程参与,其他线程等待。相当于一个单线程的过程,效率低。
只有有共享数据的代码才需要使用线程的同步解决问题。
Lock 锁方式解决线程安全问题
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁来实现同步。同步锁使用Lock对象充当。
1.使用ReentrantLock
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
// 1. 实例化 ReentrantLock
private final ReentrantLock lock = new ReentrantLock(); // 参数 fair=true/false, 公平或者非公平
@Override
public void run() {
while(true){
try {
// 2. 调用lock方法
lock.lock();
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 票号:"+ticket);
ticket--;
}else{
break;
}
} finally {
// 3. 解锁方法
lock.unlock();
}
}
}
4.5 线程安全的 懒汉式的单例模式
class Bank{
private Bank(){}
private static Bank instance = null;
// 普通获取实例的方法
public static Bank getInstance(){
if(instance==null){
instance = new Bank();
}
return instance;
}
// 线程安全的获取实例的方法一
public static synchronized Bank getInstance2(){
if(instance==null){
instance = new Bank();
}
return instance;
}
// 线程安全的获取实例的方法二,其中还有两种写法,后一种效率高些
public static Bank getInstance3(){
// 效率不高
/*
synchronized (Bank.class) {
if(instance==null){
instance = new Bank();
}
return instance;
}
*/
// 效率高一些
if(instance==null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
4.6 线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源,都在等待对方放弃自己需要的同步资源,形成了线程的死锁。
- 死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
说明:
- 出现死锁后,不会异常,不会出现提示,所有线程都处于阻塞状态,无法继续运行
- 在使用同步时,要避免出现死锁
解决方法:
- 专门的算法、原则
- 尽量较少同步资源的定义
- 尽量避免嵌套同步
示例,下面的代码会出现死锁,程序没输出没结束。在第一个线程运行到sleep的时候,线程二继续运行,结果都需要sb1 sb2,产生了死锁。
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer();
new Thread(()->{
synchronized (sb1){
sb1.append("a");
sb2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb2){
sb1.append("b");
sb2.append("2");
System.out.println(sb1);
System.out.println(sb2);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (sb2){
sb1.append("c");
sb2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (sb1){
sb1.append("d");
sb2.append("4");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}).start();
}
4.7 线程的通信
多线程协调(线程通信)问题使用 wait()
和 notify()
。
示例:两个线程交替打印1-100
/**
* 线程通信的示例:两个线程交替打印1-100
* @author chadJ
* @create 2022-03-05 22:31
*/
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while(true){
synchronized (this) {
// 唤醒其他线程
notify();
if(number<101){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
// 使得调用wait()方法的线程进入阻塞状态,并且wait回释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class ThreadCommunication {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
线程通信涉及到的方法:
wait()
一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器(锁)notity()
执行此方法,会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级最高的线程notifyAll()
执行此方法,会唤醒所有被 wait 的线程
说明:
- 上述三个方法
wait()
notify()
notifyAll()
,必须使用在同步代码块或同步方法中。Lock想要线程通信使用其他方法 wait()
notify()
notifyAll()
的调用者必须使同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException。比如
class Test implements Runnable{
...
public void run() {
synchronized (this) {
notify();
...
wait();
}
}
或
class Test implements Runnable{
...
public void run() {
synchronized (obj) {
obj.notify();
...
obj.wait();
}
}
- 这三个方法是定义在
java.lang.Object
类中的。(从上一条可知,任何一个对象都要能够调用wait()
notify()
notifyAll()
方法)
4.8 线程通信应用:生产者消费者问题
描述:生产者将产品交给店员,消费者从店员处取走商品,店员一次能持有产品数量有容量限制,如果生产者试图生产更多的产品,店员会叫停生产者,如果有空位了再通知生产者继续生产。如果店中没有商品,店员会叫停消费者,如果有产品了再通知消费者来取走产品。
分析:
- 是否是多线程问题
- 是否有共享数据
- 如何解决线程安全问题?同步机制,有三种方法
- 是否涉及到线程通信
/**
* 线程通信应用,生产者、消费者问题。 实现Runnable方式
* @author chadJ
* @create 2022-03-06 13:13
*/
/**
* 店员类
*/
class Clerk{
// 商品数量
private int goodsNum = 0;
public synchronized void createProduct() {
if(goodsNum<20){
goodsNum++;
System.out.println(Thread.currentThread().getName()+": 生产商品 "+goodsNum);
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void useProduce() {
if(goodsNum>0){
System.out.println(Thread.currentThread().getName()+": 消费商品 "+goodsNum);
goodsNum--;
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor implements Runnable{
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":开始生产商品");
while(true){
try {
Thread.sleep(30); // sleep模拟生产过程耗费的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.createProduct();
}
}
}
class Customer implements Runnable{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":开始消费商品");
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.useProduce();
}
}
}
public class ProductorCustomerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread p1 = new Thread(new Productor(clerk));
p1.setName("生产者");
p1.start();
Thread c1 = new Thread(new Customer(clerk));
c1.setName("消费者1");
c1.start();
Thread c2 = new Thread(new Customer(clerk));
c2.setName("消费者2");
c2.start();
Thread c3 = new Thread(new Customer(clerk));
c3.setName("消费者3");
c3.start();
}
}
4.9 面试题
synchronized 与 Lock 的异同?
相同:二者都可以解决线程安全的问题
不同:
- Lock 是显式锁(需要手动启动和释放锁),synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM将花费较少时间来调度线程,性能更好
优先使用顺序:(建议)
Lock → 同步代码块(已经进入了方法体,分配了相应的资源)→ 同步方法(方法体之外,作用整个方法)
如何解决线程安全问题,有几种方式
同步机制说两种三种都可以,要都讲到
两种:synchronized方式(又分同步代码块和同步方法)和Lock方式
三种:synchronized 同步代码块、synchronized 同步方法 和 Lock 方式
sleep() 和 wait() 的异同?
相同点:都可以让线程进入阻塞状态
不同点:
- 两个方法声明的位置不同:Thread 类中声明的
sleep()
;Object 类中声明的wait()
- 调用要求不同:
sleep()
可以在任何需要的场景下调用;wait()
必须使用在同步代码块或同步方法中。 - 关于是否释放同步监视器:如果两方法都使用在同步代码块或同步方法中,
sleep()
不会释放锁,wait()
会释放锁。
如何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式更强大?
call()
可以有返回值。可以抛出异常,被捕获后获取异常信息- Callable 是支持泛型的
创建多线程有几种方式?
四种:继承Thread类,实现Runnable接口,实现 Callable 接口,使用线程池(后两种为JDK5.0 新增)。