多线程
创建和启动线程方式一: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 {@Overridepublic 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() {@Overridepublic void run() {super.run();}};thread.start();// 也可以直接不起名字new Thread() {@Overridepublic 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{@Overridepublic void run() {for(int i=0;i<=100;i++){if(i%2 == 0) System.out.println(Thread.currentThread().getName()+":"+ i);}}} }
-
-
如果想再启动一个线程跑相同的事情,主需要创建一个新线程,将printEvenNumber传递为参数即可。
-
由此可以想到以匿名内部类去实现线程,如果有条件,还可以使用函数式接口去实现lambda
试了一下,你别说,还真有条件:只有一个抽象方法的接口,满足了函数式接口的条件// 配合lambdanew 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{@Overridepublic void run() {System.out.println("Thread A is running!");}}class B extends Thread{private A a;public B(A a){super(a);}@Overridepublic 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;@Overridepublic 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,这个肯定是唯一的,
@Overridepublic 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;@Overridepublic 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;@Overridepublic 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;}@Overridepublic 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;@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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()中 @Overridepublic 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