多线程
创建和启动线程方式一:Thread类
-
概述
- Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所
有的线程对象都必须是 Thread 类或其子类的实例。 - Thread 类的特性
- 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此 把 run()方法体称为线程执行体。
- 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()
- 要想实现多线程,必须在主线程中创建新的线程对象。
- Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所
-
步骤:
- 创建一个继承于Thread类的子类
- 重写run方法,
- 创建子类对象
- 通过对象调用start方法
- 启动线程
- JVM去调用线程的run方法
-
注意:
- 不能让已经start的线程再次start,否则报异常
- 如果直接在主线程里面调用run方法,相当于是方法嵌套,并没有创建新的线程
-
代码:
static class PrintEvenNumber extends Thread { @Override public void run() { for(int i=0;i<=100;i++){ if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i); } } } public static void main(String[] args) { PrintEvenNumber t1 = new PrintEvenNumber(); PrintOddNumber t2 = new PrintOddNumber(); t1.start(); t2.start(); }
-
也可以使用匿名内部类的方式创建
Thread thread = new Thread() { @Override public void run() { super.run(); } }; thread.start(); // 也可以直接不起名字 new Thread() { @Override public void run() { super.run(); } }.start();
创建和启动线程方式二:实现Runnable
-
对于继承Thread的类,他没法再去继承其他类,有一定局限性,所以有了Runnable接口。
-
步骤:
-
创建一个实现Runnable接口的类
-
实现接口中的run方法
-
创建当前实现类的对象
-
将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
-
Thread类的实例调用start方法:1.启动线程 2.调用当前线程的run方法
public class SecondWayToCreateThread { public static void main(String[] args) { PrintEvenNumber printEvenNumber = new PrintEvenNumber(); Thread thread = new Thread(printEvenNumber); thread.start(); // main方法对应的主线程 for(int i=0;i<=100;i++){ if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i); } } public static class PrintEvenNumber implements Runnable{ @Override public void run() { for(int i=0;i<=100;i++){ if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i); } } } }
-
-
如果想再启动一个线程跑相同的事情,主需要创建一个新线程,将printEvenNumber传递为参数即可。
-
由此可以想到以匿名内部类去实现线程,如果有条件,还可以使用函数式接口去实现lambda
试了一下,你别说,还真有条件:只有一个抽象方法的接口,满足了函数式接口的条件// 配合lambda new Thread(()->{ for(int i=0;i<=100;i++){ if(i%2 == 1) System.out.println(Thread.currentThread().getName()+":"+ i); } }).start();
对比两种方法
共同点:
- 启动线程,都是使用Thread类中的start方法
- 创建的线程对象,都是Thread类或其子类的实例
不同点:
- 一个是类的继承,一个是接口实现。
- 建议使用Runnable接口实现方式
- Runnable接口实现的好处:
- 实现方式避免类单继承的局限性
- 更适合处理共享数据的问题,多个线程可以共享一份数据,就是实现在Runnable继承类里面,作为Thread的入参,这样继承类里面的数据可以被多个线程共享。使用继承Thread也可以实现共享,使用静态变量也可以共享
- 数据和代码分离。run里面是处理数据,生成一个接口实现的实例就是共享数据,建立线程的时候是代码逻辑。
联系:
-
public class Thread implements Runnable(代理模式) 。我们用的Thread也是Runnable的实现类。
// 思考创建一个B(a)的线程执行run方法,结果是什么?class A extends Thread{ @Override public void run() { System.out.println("Thread A is running!"); } } class B extends Thread{ private A a; public B(A a){ super(a); } @Override public void run() { System.out.println("Thread B is running"); } } Thread a = new A(); Thread b = new B((A)a); a.start(); b.start(); // 因为A、B都继承Thread,Thread又继承了Runnable,所以这里调用super(a)等于是调用了Thread(p),生成了一个带有Runnable实现类参数的Thread对象,执行的结果会取决于p的run方法,所以执行出来会是Thread A is running!
结果: Thread A is running! Thread B is running
原因:因为B里面重载了run方法,所以优先调run重载的写法;如果把重载删除掉,就像定义Runnable实现类的实例后传参时那样,不再去重载run方法,就会默认调用Thread里面的target方法。
Thread常用方法
-
线程中的常用结构
-
线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口 中的 run 方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指 定名字。
-
线程中的常用方法系列
- public void run() :此线程要执行的任务在此处定义代码。
- public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
- public String getName() :获取当前线程名称。
- public void setName(String name):设置该线程名称。 Thread.currentThread().setName(“这样命名就行”)
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时 停止执行)。
- public static void yield():yield (主的释放CPU的执行权)只是让当前线程暂停一下,让系统的线程调度器重新 调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这 个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调 度器又将其调度出来重新执行。
- void join() :等待该线程终止。例如b线程里面调用了 a.join() 则b线程会阻塞,等待a终止之后再执行,相当于同步操作。
- void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间 到,将不再等待。
- void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
- public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未 终止,则为活动状态。
- public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入 死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据 库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处 理,出现数据不一致的问题。
- void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成 对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁 资源,导致其它线程都无法访问被它占用的锁,直到调用 resume()。已过时,不建议 使用。
-
线程优先级
- 每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
- Thread 类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下 main 线程具有普通优先 级。
- public final int getPriority() :返回线程优先级
- public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
-
守护线程
- 有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线 程被称为“守护线程”。JVM 的垃圾回收线程就是典型的守护线程。
- 守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死 亡。形象理解:兔死狗烹,鸟尽弓藏
- 用 setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前 设置,否则会报IllegalThreadStateException 异常。调用 isDaemon()可以判断线程是否是守护线程。
-
Thread生命周期
- JDK1.5 之前:5种状态:线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行 (Running)、阻塞(Blocked)、死亡(Dead)。
- JDK1.5 **及之后:**6 种状态
- NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。
- RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只 能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的, 而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
-
Teminated(被终止):表明此线程已经结束生命周期,终止运行。
- 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKED、WAITING、 TIMED_WAITING。
-
BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视 器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行 机会。
- 比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到 锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。
-
TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待 另一个线程执行一个(唤醒)动作的线程处于这一状态。
- 当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类 的 wait,LockSupport 类的 park 方法,并且在调用这些方法时, 设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间 到,或被中断。
- WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线 程执行一个特别的(唤醒)动作的线程处于这一状态。
- 当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的 join,LockSupport 类的 park 方法,并且在调用这些方法时,没 有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。
- 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的 notify/notifyAll 唤醒;
- 通过 Condition 的 await 进入 WAITING 状态的要有 Condition 的 signal 方法唤醒;
- 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有 LockSupport 类的 unpark 方法唤醒
- 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join 方法的线程对象结束才能让当前线程恢复;
- 说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现 当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。
线程安全
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条 记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如 果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
案例:火车售票问题
public class SalesTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
while (ticket>0){
System.out.println(Thread.currentThread().getName() + "售票,票号为: "+ticket--);
}
if(ticket==0) System.out.println(Thread.currentThread().getName() + "票卖完了");
}
}
class WindowTest{
public static void main(String[] args) {
SalesTicket s = new SalesTicket();
Thread t1 = new Thread(s,"窗口一");
Thread t2 = new Thread(s,"窗口二");
Thread t3 = new Thread(s,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
这样售票会出现重复售票、错票问题(卖出-1号票)的问题。
什么原因导致?
线程1操作ticket过程中,尚未结束的情况下,其他线程就参与进来,也对ticket进行操作
解决:必须保证一个a线程在操作ticket的时候,其他线程必须等待,直到线程a操作完成之后,其他才可以操作
线程同步机制:
方式一:同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
// 说明:需要被同步的代码,即操作共享数据的代码
// 共享数据:即多个线程都需要操作的数据,即ticket
// 需要被同步的代码,在被synchronized包裹之后,就使得一个线程在操作这些代码的过程中,其他线程必须等待
// 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码
// 同步监视器,可以使用任何一个类的对象充当。但是多个线程必须共用一个同步监视器
// 锁可以自己随便创建一个,也可以用this,但是要确保唯一,
// 也可以用反射,例如Window.class,这个肯定是唯一的,
@Override
public void run() {
while (true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (this){ // 此时this指向的是Runnable的继承类,唯一的
if(ticket>0){
System.out.println(Thread.currentThread().getName() + "售票,票号为: "+ticket--);
}else{
System.out.println(Thread.currentThread().getName() + "票卖完了");
break;
}
}
}
}
注意:在实现Runnable接口的方式中,锁可以考虑使用this
在Thread继承类中,锁可以慎用this。
方式二:同步方法
如果操作共享数据的代码完整的声明在了一个方法中,我们只需要将这个方法直接声明为同步方法即可。
// 对于实现Runnable的处理方式
public class SalesTicket implements Runnable {
int ticket = 100;
Object Lock = new Object();
boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
sales();
}
}
public synchronized void sales() { // 同步监视器是默认的,此时是this。这个问题中是唯一的
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (Lock) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--);
} else {
System.out.println(Thread.currentThread().getName() + "票卖完了");
isFlag = false;
}
}
}
}
同步方法默认的同步监视器(只能是默认的):
- 静态方法:当前类的 Class 对象(类名.class)
- 非静态方法:this
// 继承Thread类实现
// 此方法会出现问题,如果想解决,可以将sales改成静态方法,有些时候也不能改,例如使用了实例变量就不行。
// 或者使用同步代码块包一下,然后自己手动指定锁。
class SalesTicketThread extends Thread {
int ticket = 100;
Object Lock = new Object();
boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
sales();
}
}
public synchronized void sales() { // 同步监视器是默认的
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--);
} else {
System.out.println(Thread.currentThread().getName() + "票卖完了");
isFlag = false;
}
}
}
synchronized好处
好处:解决了线程安全问题
弊端:操作临界资源的时候,实际是串行执行的。
案例二:银行存钱,对于继承Thread的同步方法,还可以这样操作使得其同步监视器唯一:
public class AccountThread {
public static void main(String[] args) {
Account account = new Account(0);
Customer customer1 = new Customer(account, "甲");
Customer customer2 = new Customer(account, "乙");
customer1.start();
customer2.start();
}
}
class Account {
private double balance;
public Account(double b) {
this.balance = b;
}
// 此处之所以可以让非静态方法使用this,因为Account对象唯一,只创建了一个
public synchronized void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
System.out.println(Thread.currentThread().getName() + "存钱" + amount + "块,余额:" + balance);
}
}
class Customer extends Thread {
Account account;
public Customer(Account a) {
this.account = a;
}
public Customer(Account a, String name) {
super(name);
this.account = a;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.deposit(1000);
}
}
}
单例模式中的懒汉式线程安全问题
**饿汉式 **: 在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的。不存在线程安全问题
懒汉式 : 延迟创建对象,第一次调用 getInstance 方法再创建对象。存在线程安全问题(需要使用同步机制来处理)
// 实现线程安全的方式一
public static synchronized Bank getInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (instance == null) {
instance = new Bank();
}
return instance;
}
// 实现线程安全的方式二
public static Bank getInstance() {
synchronized (Bank.class) {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Bank();
}
}
return instance;
}
// 实现线程安全的方式三,相较与方式一二效率更高,不需要卡在不必要的同步锁之前,直接判断即可。
// 对于每次访问,原本都需要串行化,现在只有为创建时的访问的线程需要串行
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Bank();
}
}
}
return instance;
}
/*
注意:上述方式 3 中,有指令重排问题
mem = allocate(); 为单例对象分配内存空间
instance = mem; instance 引用现在非空,但还未初始化 ctorSingleton(instance); 为单例对象通过 instance 调用构造器
从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要 volatile 关键字,避免指令 重排。
*/
private static volatile Bank instance = null;
方式三虽然效率高,但是存在问题:创建对象的语句不止一步,先进行创建,然后进行初始化。创建了对象之后,判断已经不是null了,但是还没有初始化完成,所以直接让其他线程拿走instance是不对的。
volatile作用:可见性和有序性。一旦变化,其他线程都能看见;并且对他的操作都是有序的。
同步机制的死锁问题
- 死锁概念:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
- 诱发死锁的原因:
- 互斥条件(互斥访问)
- 占用且等待
- 不可抢夺
- 循环等待
- 解决死锁:
- 死锁一旦出现,基本很难人为干预,只能尽量规避,可以考虑打破上面的诱发条件。
- 针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问 题。
- 针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问 题。
- 针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到, 就主动释放掉已经占用的资源。
- 针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这 样避免循环等待问题。
- jkd5.0提供的Lock锁的方式:解决线程安全
- 步骤:
// 1.这里一定需要静态的,不然三个Thread对象,三把锁,依然无效 private static final ReentrantLock lock = new ReentrantLock(); try { // 2. 执行lock方法,锁定对共享资源的调用 lock.lock(); try { Thread.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket--); } else { System.out.println(Thread.currentThread().getName() + "票卖完了"); isFlag = false; } // 使用finally确保一定执行解锁 } finally { // 3.unlock()的调用,释放对共享数据的锁定 lock.unlock(); }
- 步骤:
线程通信
-
线程间通信
当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那 么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同 操作一份数据。 -
等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争 (race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)), 等 待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可 以使用 *notifyAll()*来唤醒所有的等待线程。wait/notify 就是线程间的一种协 作机制。
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它 还要等着别的线程执行一个特别的动作,也即“通知(notify)”或者等待时间 到,在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列 (ready queue)中
- notify :一旦执行次方法,就会唤醒被wait()的线程中优先级最高的那一个线程。如果被wait的多个线程的优先级相同,则随机唤醒一个。被唤醒的线程,从当初被wait的位置的代码逻辑继续执行。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
- 使用注意:
- 谁在调用wait和notify?凡是在方法中,没有写谁来调的,如果是非静态方法就是this的省略,如果是静态方法就是当前类在调用。
- 次三个通信方法的使用必须在同步代码块或同步方法中。Lock是不行的。Lock通信需要配合condition实现线程间通信。
- 此三个方法的调用者,必须是同步代码块或者同步方法的监视器。所以Lock不可以,因为他没有同步监视器。所以也不是必须要this来调用notify和wait,也可以声明一个唯一的object来调用。
- 此三个方法声明在Object类中。native方法。
-
比较wait和sleep方法:
- 相同点:一旦执行,当前线程都会进入阻塞状态。
- 不同点:
- 声明位置:wait声明在Object中的,sleep声明在Thread中,并且是一个静态方法。
- 场景不一样:wait只能使用在同步代码块和同步方法中。sleep可以使用在任何地方
- 使用在临界资源中时:wait会释放同步监视器(锁),而sleep不会释放。
- 结束阻塞的方法:wait两种情况,到达指定时间自动唤醒或被notify唤醒而结束阻塞。sleep只有超时唤醒。
-
案例一:让两个线程交替打印
class PrintNumber implements Runnable{ private int number; @Override public void run() { while(true){ synchronized (this) { notify(); // 唤醒操作 if(number<=100){ try { Thread.sleep(10); // sleep并不会释放同步监视器,就可能会出现死锁 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" : "+number++); try { wait(); // 线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用 } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } }
-
案例二:生产者&消费者案例
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产 品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的 产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生 产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通 知消费者来取走产品。
public class ProducerConsumerTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p = new Producer(clerk,"生产者1"); Consumer c1 = new Consumer(clerk,"消费者1"); Consumer c2 = new Consumer(clerk, "消费者2"); p.start(); c1.start(); //c2.start(); } } class Clerk { private int productNum = 0; public synchronized void addProduct() { if (productNum < 20) { System.out.println(Thread.currentThread().getName() + "生产了第" + ++productNum + "个产品"); notifyAll(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void minusProduct() { if (productNum > 0) { System.out.println(Thread.currentThread().getName() + "消费了第" + productNum-- + "个产品"); notifyAll(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread { private Clerk clerk; public Producer(Clerk clerk, String name) { super(name); this.clerk = clerk; } @Override public void run() { while (true) { try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } clerk.addProduct(); } } } class Consumer extends Thread { private Clerk clerk; public Consumer(Clerk clerk, String name) { super(name); this.clerk = clerk; } @Override public void run() { while (true) { try { Thread.sleep(30); } catch (InterruptedException e) { throw new RuntimeException(e); } clerk.minusProduct(); } } }
Callable
- 与使用 Runnable 相比, Callable 功能更强大些
- 相比 run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
- Future 接口(了解)
- 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完 成、获取结果等。
- FutureTask 是 Futrue 接口的唯一的实现类
- FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被 线程执行,又可以作为 Future 得到 Callable 的返回值
- 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
- 代码:
/** * 创建多线程的方式三:实现 Callable (jdk5.0 新增的) **/ //1.创建一个实现 Callable 的实现类 class NumThread 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; } } public class CallableTest { public static void main(String[] args) { //3.创建 Callable 接口实现类的对象 NumThread numThread = new NumThread(); NumThread numThread = new NumThread(); //4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中, 创建 FutureTask 的对象 FutureTask futureTask = new FutureTask(numThread); //5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Th read 对象,并调用 start() new Thread(futureTask).start(); // 接收返回值 try { //6.获取 Callable 中 call 方法的返回值 //get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。 Object sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
线程池
-
现有问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务? - 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
-
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管,可以设置相关参数
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
-
线程池相关 API
- JDK5.0 之前,我们必须手动自定义线程池。从 JDK5.0 开始,Java 内置线程池相关的 API。在 java.util.concurrent 包下提供了线程池相关 API:ExecutorService 和 Executors。
-
ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
- void execute(Runnable command)* :执行任务/命令,没有返回值,
一般用来执行 Runnable - Future submit(Callable task):执行任务,有返回 值,一般又来执行 Callable
- void shutdown() :关闭连接池
- void execute(Runnable command)* :执行任务/命令,没有返回值,
-
Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
- Executors.newCachedThreadPool():创建一个可根据需要创建新线 程的线程池
- Executors.newFixedThreadPool(int nThreads); 创建一个可重用 固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的 线程池
- Executors.newScheduledThreadPool(int corePoolSize):创建 一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
面试题目:
public static void main(String[] args) {
Thread t1 = new Thread(()->{
...
});
t1.start();
try {
t1.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("End");
}
- 对于上面这段代码:sleep()阻塞的是哪个进程?
- 答案:主进程。调用sleep时,就当作是一个对象的方法调用即可,并不是调用谁的sleep谁就会阻塞,而是看逻辑而言,整个处理逻辑是在主进程中的。
- synchronized同步方式与Lock对比?
- Lock更好一些,释放对共享数据的方式不一样,更加灵活,synchronized释放锁(同步监视器)必须等到大括号结束。
- Lock是一个接口,提供了多种实现类,适合更多更复杂的场景,效率更高一些。
- 多线程使用场景
- Tomcat服务器上的web应用,多个客户端发起请求,对每个请求开辟线程进行处理
- 如何实现多线程(实现多线程有哪些方式):4种
类似问题:创建多线程用Runnable还是Thread - start和run方法区别:
- start:开启线程;调用run方法
- run:相当于没有启动新线程,只是方法调用
- Runnable和Callable有什么不同?
- 与使用 Runnable 相比, Callable 功能更强大些
- 相比 run()方法,call()方法可以有返回值
- call()方法可以抛出异常
- 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
- 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
- 与使用 Runnable 相比, Callable 功能更强大些
- 什么是线程池,为什么用它?(好处)
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管,可以设置相关参数
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- sleep()和yield()区别?
- sleep:调用进入阻塞,有限时间等待,进入TIMED_WAITING
- yield:释放cpu执行权,但是还在runnable状态下
- 线程创建中的方法、属性情况(略)
- 线程声明周期(略)
- 线程基本状态和以及状态直接的转化(略)
- stop和suspend方法为什么不推荐?
- stop:执行,线程就结束,可能导致run有未执行的代码。会释放同步监视器,导致线程安全问题
- suspend:搭配resume使用,可能导致死锁
- 优先级定义:三个常量,并且java是抢占式的
- 如何理解线程安全,线程安全问题是如何造成的?
- 保证线程安全问题的方式:
- 同步机制 synchronized
- 代码块
- 方法
- Lock接口
- 同步机制 synchronized
- synchronized修饰静态方法和普通方法的区别:同步监视器不同
- 当一个线程进入一个对象的一个synchronized方法后,其线程是否可以进入此对象的其他方法
- 答:需要看其他方法是否使用了synchronized修饰,并且还需要看他同步监视器和刚才进入的是不是一个(this或类对象)
- 线程阻塞与同步的关系?同步一定阻塞吗?阻塞一定同步吗?
- 答:同步一定阻塞,阻塞不一定是因为同步,可能是sleep等
- 什么是死锁,产生死锁等原因和必要条件,如何避免死锁
- notify和notifyAll有什么区别?
-
为什么wait和notify要放在同步代码块调用
- 同步监视器才可以调用wait和notify
- 确保多线程通信和协调的可靠性
- wait和notify操作需要保证其原子性
- 例如:一个生产者生产出产品后立刻唤醒消费者,如果不保证原子性,被其他消费者消费后,再去唤醒的消费者依然吃不到产品。
- wait和notify操作需要保证其原子性
- 单例模式线程安全
- 饿汉式是线程安全的
- 懒汉式单例安全:需要对创建instance的部分加同步,并且在同步代码块外层加一层判断用以加快效率,但是有可能创建+初始化比较慢,需要加volatile