多线程
1. 多线程概述
1.1 进程和线程
- 进程
- 一个应用程序(一个软件)
- 线程
- 线程是一个进程中的执行场景(执行单元)
- 一个进程可以启动多个线程
- 举例
- 在DOS命令窗口输入 java HelloWorld 之后(运行HelloWorld类)
- 会先启动JVM,而JVM就是一个进程
- JVM再调用一个主线程调用main方法
- 同时调用一个垃圾回收线程负责看护,回收垃圾
1.2 进程和线程的关系
-
阿里巴巴:进程
- 马云:阿里巴巴的一个线程
- 阿里员工:阿里巴巴的一个线程
-
京东:进程
- 强东:京东的一个线程
- 京东员工:京东的一个线程
-
进程可以看作公司,线程可以看作公司中某个员工
-
进程A和进程B
- 内存独立不共享(阿里巴巴和京东资源不共享)
-
同一进程中的线程a和线程b
- 堆内存和方法区内存共享
- 栈内存独立,一个线程一个栈
-
多线程并发
- 假设有10个线程,那么会有10个栈,互不干扰,各自执行各自的任务
- 目的是提高程序的处理效率
-
main方法结束
- 使用多线程机制之后,main方法结束只是一个主线程结束,主栈空了
- 其它的线程仍然在压栈,弹栈
1.3 单核CPU和多核CPU实现多线程
- 多核CPU真正实现多线程是没问题的
- 多核相当于多个大脑,可以真正的有多个进程同时并发
- 比如4核CPU同一时间可以真正的有4个进程同时并发
- 单核CPU
- 只有一个大脑,不能真正的多线程并发
- 但可以给人一种多线程并发的假象
- 只需要快速的在a线程和b线程之间来回切换,快到人脑反应不过来,就可以制造这种感觉
- 就像很多图片以足够快的速度连续放映就像真的动起来一样
2. 实现线程的两种方式
- 主要的目的是传入run方法
- 第一种使用重写的方式
- 第二种使用实现接口并传入构造方法的方式
注意:
- 使用第二种方法的比较多
- 因为实现了接口还可以继承其它的类,更灵活
2.1 第一种方式
-
编写一个类,直接继承 java.lang.Thread 重写run方法
-
run方法在分支进程的栈的最底部,地位等同于主栈中的main方法
-
start方法作用是在JVM开辟一个新的栈空间,建立一个分支线程,建立完start方法就结束
public class Main {
public static void main(String[] args) {
//main方法在主线程,在主栈中运行
//新建一个分支线程对象
Mythread mythread=new Mythread();
mythread.run();//这里如果直接执行run方法,并没有开辟新的栈空间,还是在一个线程里
//在JVM开辟一个新的栈空间,开辟完之后,start方法结束
//mythread.start();
//下面的代码还是在主线程中运行,run方法在分支线程运行
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
}
class Mythread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
}
- 如果新开辟了线程,两个线程互不干扰,但只有一个控制台,所以输出的时候会交替,不均匀的输出
2.2 第二种方式
- 编写一个类,实现Runnable接口
- 创建线程对象,传入可运行类,调用线程对象的start方法
public class Main {
public static void main(String[] args) {
//创建一个线程对象,放入一个可运行对象
Thread T=new Thread(new MyRunnable());
//启动线程
T.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
}
- 匿名内部类写法
public class Main {
public static void main(String[] args) {
//创建一个线程对象,放入一个可运行对象
Thread T=new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+i);
}
}
});
//启动线程
T.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->"+i);
}
}
}
- 原理内存图
3. 线程的生命周期
4. 线程常用方法
- 线程有默认的名字
- Tread-0
- Tread-1…
4.1 获取线程对象的名字
myThread.getName();
4.2 修改线程对象的名字
myThread.setName("tttt");
4.3 获取当前线程对象
Thread T=Thread.currentThread();
- 测试
public class Main {
public static void main(String[] args) {
//获取当前线程对象
//出现在main方法,所以获取的就是主线程
Thread T=Thread.currentThread();
System.out.println(T.getName());//main
//创建线程对象
MyThread myThread=new MyThread();
String name=myThread.getName();
System.out.println(name);//Thread-0
//设置线程的名字
myThread.setName("tttt");
//重新获取名字
name=myThread.getName();
System.out.println(name);
//启动线程
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//获取调用run方法的线程
//谁调用,获取的就是谁
Thread T=Thread.currentThread();
System.out.println(T.getName()+"--->"+i);
}
}
}
4.4 线程的sleep方法
4.4.1 sleep基本用法
static void sleep(long millis)
- sleep是一个静态方法
- 作用
- 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用
- 出现在哪个线程就让哪个线程休眠
- 可以让某段代码间隔一定的时间在去执行
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
Thread T=Thread.currentThread();
String name=T.getName();
//每隔一秒输出一行
System.out.println(name+"--->"+i);
try {
Thread.sleep(1000);//休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- sleep面试题
public class Main {
public static void main(String[] args) {
//创建线程对象
Thread t=new Mythread();
t.setName("t");
t.start();
//调用sleep方法
try {
t.sleep(1000*5);//关键点
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里是否会休眠?
System.out.println("hello");
}
}
class Mythread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
- t.sleep(1000*5),按理说是不能这样调用的
- 因为sleep是静态方法,只能通过“类名.”的方式调用
- 如果通过引用来调用,编译器会自动转换为 Thread.sleep()
- 本质是在main方法调用Thread.sleep()方法
- 所以主线程main会休眠五秒,与线程t无关
4.4.2 终止线程的睡眠
- interrupt
- 线程终止,同时抛出线程终止异常
- stop(已过时)
- 存在很大的缺点
- 原理是直接将线程杀死
- 线程没有报错的数据将直接丢失,不建议使用
public class Main {
public static void main(String[] args) {
//实现Runnable接口的方式创建线程对象
Thread t=new Thread(new MyRunnable());
t.start();
//5秒之后让t线程终止休眠
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t线程终止,同时抛出线程终止异常
t.interrupt();
//t.stop(); 强行终止,容易丢失数据,已过时
}
}
class MyRunnable implements Runnable {
//run中的异常不能throw抛出
//因为父类run方法没有抛出异常,子类不能比父类抛出更多异常
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---> begin");
try {
Thread.sleep(1000*60*60*24*365);//休眠一年
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.4.3 一种合理的,常用的终止方法
public class Main {
public static void main(String[] args) {
MyRunnable m=new MyRunnable();
//传入实现Runnable接口的类,创建线程对象
Thread t=new Thread(m);
//启动线程
t.start();
//模拟5秒后终止线程
try {
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
m.run=false;
}
}
class MyRunnable implements Runnable{
//用来控制线程的终止与正常运行
boolean run=true;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if(run){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
else {
//终止之前在这里保存还没保存的数据
//save...
//终止线程
return;
}
}
}
}
5. 线程调度
5.1 常见的线程调度模型
- 抢占式调度模型
- 哪个线程的优先级较高,抢到的CPU时间片就多一些/概率高一些
- Java采用的就是抢占式调度模型
- 均分式调度模型
- 平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样
- 有些编程语言采用这种方式
5.2 Java提供的与线程调度有关的方法(了解)
- 实例方法
void setPriority(int newPriority);//设置线程的优先级
int getPriority();//获取线程的优先级
最低优先级 1
默认优先级5
最高优先级10
- 静态方法
static void yield();//让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用
yield()方法的执行会让当前线程从 运行状态 回到 就绪状态(还有可能再次抢到)
- 实例方法
void join();//合并线程
t.join();
当前线程进入阻塞状态,t线程开始执行,t线程结束后,当前线程才能继续执行
6.多线程并发环境下,数据安全问题(重要)
- 最重要的就是
- 自己编写的程序放在多线程环境下是否安全
6.1 多线程的安全问题
- 出现安全问题的三个条件
- 多线程并发
- 有共享数据
- 共享数据有修改的行为
6.2 解决线程安全问题
- 线程排队执行(线程同步机制)
- 这样会牺牲一部分效率,但为了数据安全,必须这么做
6.3 同步编程模型和异步编程模型
- 同步编程模型
- 线程排队执行(一个线程在执行,另一个需要排队,不能同时执行)
- 异步编程模型
- 多线程并发(两线程互不干扰,各自执行)
6.4 通过代码模拟线程安全隐患
- 编写账户类
public class Account {
//账户
private String actno;
//余额
private double balance;
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//取款之前的余额
double before=balance;
//取款之后的余额
double after=balance-money;
//模拟网络延迟1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}
- 编写账户线程类
public class AccountThread extends Thread{
//两个线程共享的账户对象
private Account act;
//通过构造方法传递账户对象
AccountThread(Account act){
this.act=act;
}
//run方法
public void run(){
//表示取款操作
//每次取款5000
double money=5000;
act.withdraw(money);
System.out.println(Thread.currentThread().getName()+"对"+act.getActno()+"取款"+money+" 余额"+act.getBalance());
}
}
- 测试,主方法
public class Main {
public static void main(String[] args) {
//创建共享账户对象
Account act=new Account("act-01",10000);
//创建两个线程对象,共用一个账户
Thread t1=new AccountThread(act);
Thread t2=new AccountThread(act);
//设置线程名字
t1.setName("t1");
t2.setName("t2");
//启动线程
t1.start();
t2.start();
}
}
- 运行结果
t1对act-01取款5000.0 余额5000.0
t2对act-01取款5000.0 余额5000.0
- 正确的取款顺序
- 余额10000,取钱5000,更新余额为5000
- 余额5000,取钱5000,更新余额为0
- 有延迟的情况下取款顺序
- 余额10000,取钱5000
- 余额10000,取钱5000
- 更新余额为5000
- 更新余额为5000
6.5 用synchronized解决问题(同步机制)
- 如果让取钱的线程对象排队
- 等一个线程对象完整的进行取钱操作,更新余额后,下一个用户在进行取钱操作
- 这样就可以避免余额更新延迟带来的损失
synchronized(填入需要排队的线程所共享的对象内存地址){
//排队执行的代码块
}
- 修改后的取钱方法
//取款方法
public void withdraw(double money){
synchronized (this){
//取款之前的余额
double before=balance;
//取款之后的余额
double after=balance-money;
//模拟网络延迟1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}
- 运行结果
t1对act-01取款5000.0 余额5000.0
t2对act-01取款5000.0 余额0.0
6.6 synchronized的深入理解
-
锁的概念
- 一个对象有一把锁
- 锁是一个标志,只是取个名字叫锁
- t1和t2排队执行synchronized代码块中的语句,假设t1先进入,会占有synchronized()括号中对象的锁,执行完后释放锁
- 相当于t1和t2共享同一个坑,t1先上厕所,然后锁门(占有锁),只有上完出去才会把锁打开(释放锁)
-
synchronized()括号中参数的理解
-
该线程会去锁池找括号内的对象的锁(可以理解为一种阻塞状态),有几把找几把
-
括号中传入的具体是谁不重要,可以是普通对象也可以是一个字符串对象(只有一把锁,字符串常量池所有线程共享,都排队)
-
重要的是他到底有几把锁(是几个对象),以及线程的共享关系
- 如果这个对象唯一,只有一把锁,都要排队
- 如果有两个对象,t1和t2共享一个对象(共享一把锁,操作同一个对象),t3有另一个对象(自己用一把锁)
- t1和t2排队,t3不用排队,自己用
-
synchronized使用在实例方法上(共享对象默认this)
public synchronized void withdraw(){}
- 优点
- 代码量减少
- 如果整个方法都需要同步,而且共享的对象是this,建议使用这种简洁的写法
- 缺点
- 整个实例方法同步,扩大同步范围,效率降低
6.7 不会发生安全问题的变量
-
三大变量
- 实例变量 堆区 共享
- 静态变量 方法区 共享
- 局部变量 栈中 不共享
-
局部变量,常量不会发生安全问题
- 栈中的数据是不共享的,一个线程一个栈
- 所以局部变量不会发生安全问题
- 常量不能改变,也不会发生安全问题
-
建议
- 建议使用StringBuilder,因为局部变量不存在线程安全问题
- StringBuffer某些实例方法中用了关键字synchronized,效率比较低
- 线程安全的
- Vector
- HashTable
- 非线程安全的
- ArrayList
- HashMap
- HashSet
6.8 总结synchronized的三种写法
-
同步代码块
-
synchronized(线程共享对象){ 同步代码块; }
-
-
实例方法上使用synchronized
- 共享对象一定是this
- 同步代码块是整个方法体
-
静态方法上使用synchronized
- 表示找到类锁
- 类锁只有一把,创建多少对象都是一把
6.9 synchronized面试题
第一题
- 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
public static void main(String[] args) {
Myclass test=new Myclass();
//两个线程共享一个对象
MyThread t1=new MyThread(test);
MyThread t2=new MyThread(test);
//给线程对象设置名称
t1.setName("t1");
t2.setName("t2");
//线程t1启动
t1.start();
//延迟一秒,为了让t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程t2启动
t2.start();
}
}
class MyThread extends Thread{
//线程对象中的一个共享对象属性
Myclass test;
MyThread(Myclass test){
this.test=test;
}
//线程t1调用doSome
//线程t2调用doOther
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")){
test.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
test.doOther();
}
}
}
class Myclass{
//上锁
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
-
不需要,因为虽然只有一把锁,t1占用了,但doOther方法不需要锁的限制,直接执行
-
执行顺序
-
doSome begin doOther begin doOther over doSome over
-
第二题
- 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
public static void main(String[] args) {
Myclass test=new Myclass();
//两个线程共享一个对象
MyThread t1=new MyThread(test);
MyThread t2=new MyThread(test);
//给线程对象设置名称
t1.setName("t1");
t2.setName("t2");
//线程t1启动
t1.start();
//延迟一秒,为了让t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程t2启动
t2.start();
}
}
class MyThread extends Thread{
//线程对象中的一个共享对象属性
Myclass test;
MyThread(Myclass test){
this.test=test;
}
//线程t1调用doSome
//线程t2调用doOther
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")){
test.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
test.doOther();
}
}
}
class Myclass{
//上锁
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
//上锁
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
-
这题两个方法都上锁了
-
需要,因为一个对象只有一把锁,t1占用了
-
t2执行doOther方法的时候同样需要这把锁,但被占用,所以需要等待doSome结束,把锁释放
-
执行顺序
-
doSome begin doSome over doOther begin doOther over
-
第三题
- 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
public static void main(String[] args) {
Myclass test1=new Myclass();
Myclass test2=new Myclass();
//两个线程两个对象
MyThread t1=new MyThread(test1);
MyThread t2=new MyThread(test2);
//给线程对象设置名称
t1.setName("t1");
t2.setName("t2");
//线程t1启动
t1.start();
//延迟一秒,为了让t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程t2启动
t2.start();
}
}
class MyThread extends Thread{
//线程对象中的一个共享对象属性
Myclass test;
MyThread(Myclass test){
this.test=test;
}
//线程t1调用doSome
//线程t2调用doOther
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")){
test.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
test.doOther();
}
}
}
class Myclass{
//上锁
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
//上锁
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
-
这题new了两个对象,不共享对象
-
不需要等待,因为两个对象两把锁
-
虽然两个方法都需要锁,但现在有两个锁,自己拿自己的锁,不需要等待
第四题
- 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
public static void main(String[] args) {
Myclass test1=new Myclass();
Myclass test2=new Myclass();
//两个线程共享一个对象
MyThread t1=new MyThread(test1);
MyThread t2=new MyThread(test2);
//给线程对象设置名称
t1.setName("t1");
t2.setName("t2");
//线程t1启动
t1.start();
//延迟一秒,为了让t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程t2启动
t2.start();
}
}
class MyThread extends Thread{
//线程对象中的一个共享对象属性
Myclass test;
MyThread(Myclass test){
this.test=test;
}
//线程t1调用doSome
//线程t2调用doOther
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")){
test.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
test.doOther();
}
}
}
class Myclass{
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
-
需要
-
加了static关键字之后占用的是类锁
-
虽然有两个对象,但类锁只有一把,t1占用了类锁,t2需要等待
-
执行顺序
-
doSome begin doSome over doOther begin doOther over
-
6.10 开发中如何解决线程安全问题
-
尽量不用synchronized,这样减少并发,降低效率
-
尽量使用 局部变量 代替 实例变量 和 静态变量
-
如果必须是实例变量,考虑创建多个对象,这样就不会有内存共享
-
不能用局部变量,也不能创建多个对象,这时候在选择使用synchronized
7. 死锁
- 死锁程序
- 不会出现异常,不报错,一致僵持,永远不结束
public class Main {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
MyThread1 t1=new MyThread1(o1,o2);
MyThread2 t2=new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
MyThread1(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
MyThread2(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
8. 守护线程
8.1 守护线程
-
Java语言线程分类
- 用户线程
- 例如main方法
- 守护线程(后台线程)
- 死循环,所以用户线程结束,守护线程才结束
- 例如
- 垃圾回收线程
- 定时自动备份
- 用户线程
-
模拟守护线程
public class Main {
public static void main(String[] args) {
Thread t=new MyThread();
t.setName("守护线程");
//设置为守护线程
t.setDaemon(true);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread{
@Override
public void run() {
int i=0;
//死循环,用户线程结束,守护线程结束
while (true){
System.out.println(Thread.currentThread().getName()+"------>"+(++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
8.2 定时器
-
定时器的作用
- 特定时间执行特定程序
-
定时器的实现
- 原始的方法
- 使用sleep方法,设置睡眠时间
- 实际开发中
- java.util.Timer(使用的较少)
- Spring框架中的SpringTask框架(使用较多)
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class Main { public static void main(String[] args) throws ParseException { //创建定时器对象 Timer timer=new Timer(); //指定定时任务 SimpleDateFormat sdf=new SimpleDateFormat("yyyy 年 MM 月 dd 日 hh 时 mm 分 ss秒"); //设置启动时间,转换为date类型 Date firstTime=sdf.parse("2021 年 05 月 30 日 04 时 39 分 00秒"); //三个参数:执行任务,起始时间,间隔时间 timer.schedule(new LogTimerTask(),firstTime,1000*10); //这里TimerTask是抽象类,不能直接new,可以用匿名内部类,也可以写个类继承 } } class LogTimerTask extends TimerTask{ @Override public void run() { SimpleDateFormat sdf=new SimpleDateFormat("yyyy 年 MM 月 dd 日 hh 时 mm 分 ss秒"); String strTimer=sdf.format(new Date()); System.out.println(strTimer+":完成了异常数据备份"); } }
- 原始的方法
9. 实现线程的第三种方式
- 实现Callable接口
- 优点:
- 可以获取当前线程的执行结果
- 缺点:
- 效率比较低,获取结果的时候可能导致线程受阻
- 优点:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步,创建一个未来任务对象
//参数需要给一个Callable接口
FutureTask task=new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method over");
int a=100;
int b=200;
return a+b;
}
});
//传入FutureTask,创建线程对象(类似传入Runnable接口)
Thread t=new Thread(task);
//启动线程
t.start();
//获取t线程的返回结果
//get方法的执行可能会导致当前线程的阻塞
Object obj=task.get();
System.out.println(obj);//300
}
}
10. wait和notify方法
-
wait和notify方法
- 两个方法是所以对象都有的方法,因为它们是Object类中的方法
- 这两个方法不是通过线程对象调用的
-
wait方法作用
-
Object o=new Object(); o.wait;
-
表示让o对象进入等待状态,并释放锁,直到被唤醒
-
-
notify方法作用
-
Object o=new Object(); o.notify;
-
唤醒o对象,不释放锁
-
notifyAll方法
- 唤醒o对象处于等待的所有线程
-
10.1 生产者和消费者模式
- 这种模式是实际开发中可能遇到的情况
- 模拟生产者和消费者模式
- 生产一个消费一个
- 如果仓库有货,停止生产,消费后在继续生产
- 如果仓库没货,停止消费,生产后在继续消费
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
//创建一个仓库对象
List list=new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1=new Thread(new Producer(list));
//消费者线程
Thread t2=new Thread(new Consumer(list));
//设置线程名称
t1.setName("生产者线程");
t2.setName("消费者线程");
//启动线程
t1.start();
t2.start();
}
}
//生产线程
class Producer implements Runnable{
//仓库
List list;
public Producer(List list){
this.list=list;
}
@Override
public void run() {
//死循环一直生产
while (true){
//加锁的目的是使生产和消费的代码块不能同时执行
synchronized (list){
//如果仓库有货,停止生产,消费后在继续生产
if(list.size()>0){
//进入线程等待状态,并且释放锁,让消费者线程消费
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序执行到这说明仓库没货,进行生产
Object obj=new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
//货满了,需要唤醒消费者消费,如果不唤醒,都在等待,僵持住
//这里都唤醒了不会出问题,因为就算生产者被唤醒之后又立马抢到了锁,由于仓库是空的
//会立马又进入等待状态,不会多生产
list.notifyAll();
}
}
}
}
//消费线程
class Consumer implements Runnable{
//仓库
List list;
public Consumer(List list){
this.list=list;
}
@Override
public void run() {
//如果有货一直消费
while(true){
synchronized (list){
//如果仓库空,就停止消费
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序走到这说明仓库有货
Object obj=list.remove(0);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
//消费后就没货了,唤醒消费线程生产
list.notifyAll();
}
}
}
}