线程的调度
Java中提供的那些方法是和线程调度有关的呢?
实例方法:
-
void setpriority ( int newpriority) 设置线程的优先级
-
int getpriority() 获取线程的优先级
最低优先级是1
默认优先级是5
最高优先级是10
优先级比较高的获取CPU时间片可能会多一些(但也不完全是,大概率情况下是会高一些)
静态方法
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其它线程
yield方法不是阻塞方法。让当前线程让位,让给其他的线程使用
yield方法会让当前线程从“运行状态”回到“就绪状态”
注意:在回到就绪之后有可能会再次抢到
实例方法:
void join()
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t = new MyThread2();
try {
t.join();//表示当前线程进入阻塞状态,t线程执行,直到t线程结束,当前线程才可以正常执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread2 extends Thread{
}
线程的安全
关于多线程并发环境下,数据的安全问题
2.1、为什么这个是重点?
以后在开发中,我们的项目都是运行在服务器当中的,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
最终的是我们要知道自己写的代码是要放到多线程的环境下运行的,更需要关注我们的数据在多线程并发的环境下是否是安全的(*****)
2.2什么情况下数据在多线程并发时会存在安全问题呢?
三个条件:
-
条件一:多线程并发
-
条件二:有共享数据
-
条件三:共享数据有修改的行为
满足以上三个条件之后就会存在线程安全问题
2.3怎么解决线程的安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就会存在线程安全问题,怎么解决这个问题?
线程排队执行
使用排队执行解决线程安全问题
这种机制被称作:线程同步机制
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行
怎么解决线程安全问题?
使用“线程同步机制”
线程同步就是线程排队,线程排队了就会牺牲一定的效率,没办法,数据安全第一位,只有数据安全了,我们才考虑效率。
2.4、同步编程模型和异步编程模型
-
异步编程模型:
线程t1和线程t2各自执行各自的,t1不管t2,t2也不管t1,谁也不需要等谁,这种编程模型叫做异步编程模型。其实就是:多线程并发(效率较高)
异步就是并发
-
同步编程模型:
线程t1和线程t2,在线程t1执行的时候必须等待线程t2执行结束,或者说在线程t2执行的时候必须等待线程t1执行结束。两个线程之间发生了等待关系,这就是同步编程模型。(效率较低,线程排队执行)
同步就是排队
银行账户的模拟
编写程序模拟两个线程同时对一个账户进行取款操作
package ThreadAccountSimulation;
/*
银行账户
不使用线程同步机制,多线程同时对一个账户进行操作,出现线程安全问题
*/
public class Account {
private String actno;//账户
private double balance;//余额
public Account() {
}
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 = this.getBalance();
//取款之后的余额
double after = before - money;
//在这里模拟一下网络延迟,100%会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这里了,但还没有来得及执行下一行代码,t2进来withdraw方法了,此时一定会出问题
this.setBalance(after);
}
}
package ThreadAccountSimulation;
public class AccountThread extends Thread{
//两个线程必须共享一个账户对象
private Account act;
//通过构造方法传递账户对象
public AccountThread(Account act){
this.act = act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
this.act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对账户" + act.getActno() + "取款成功,余额" + act.getBalance());
}
}
package ThreadAccountSimulation;
public class Test {
public static void main(String[] args) {
//创建账户对象
Account account = new Account("act-001",10000);//只创建一个
//创建两个线程
Thread t1 = new AccountThread(account);
Thread t2 = new AccountThread(account);
//设置线程名字
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
上述测试的结果表示了在不使用线程同步机制情况下,多线程并发造成的问题,运行结果如下:
t1对账户act-001取款成功,余额5000.0
t2对账户act-001取款成功,余额5000.0
Process finished with exit code 0
下面使用线程同步机制,对上述的代码进行修改,消除漏洞隐患,解决线程安全问题
只需对Account类进行修改即可,其他的两个类保持不变
package ThreadAccountSimulation2;
/*
银行账户
使用线程同步机制,解决线程安全问题
*/
public class Account {
private String actno;//账户
private double balance;//余额
public Account() {
}
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){
//取款的这部分代码从开始到结束只能有一个线程执行
//即以下这几行代码必须是线程排队的,不能并发
//一个线程把这里的代码全部执行完毕后,另一个线程才能进来
/*
线程同步机制的语法是:
synchronized(){
//线程同步代码块
}
synchronized后面的小括号()中传的“数据”是相当重要的,
这个线程必须是多线程共享的数据,才能达到线程多线程排队
()中写什么?
那要看你想让哪些线程同步。
假设有t1,t2,t3,t4,t5,有5个线程,你只希望t1,t2,t3排队,t4t5不需要排队。怎么办?
你一定要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4,t5来说不是共享的。
这里的共享对象:账户对象
账户对象是共享的,那么this就是账户对象吧
*/
synchronized (this){
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
上述代码的运行结果为:
t1对账户act-001取款成功,余额5000.0
t2对账户act-001取款成功,余额0.0
在java语言中,任何对象都有一把锁,其实这把锁就是标记(只是把它叫做“锁”)
100个对象,100把锁。一个对象一把锁
上述代码的执行原理是:
-
假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先,一个后
-
假设t1先执行了,遇到了synchronized,这时候自动找后面对象的对象锁
找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直是占有这把锁的。直到同步代码块代码结束,这把锁才会释放
-
假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有后面的共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序
这样就达到了线程的排队执行
这里要注意的是:这个对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
Java中有三大变量:【重要问题】
- 实例变量:在堆中
- 静态变量:在方法区
- 局部变量:在栈中
以上三大变量中:
局部变量永远不会存在线程安全问题
因为局部变量不共享。(一个线程一个栈)
局部变量在栈中,所以局部变量永远不会共享
实例变量在堆中,堆只有一个
静态变量在方法区中,方法区只有一个
堆和方法区都是多线程共享的,所以可能存在线程安全问题
在AccountThread中做下面的修改也可以达到相同的目的:
synchronized出现在实例方法中:
在实例方法上可以使用synchronized吗?可以的
- 缺点
synchronized出现在实例方法上,一定锁的是this,不能是其他的对象了。
这种方式灵活性较差。
另外一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步
可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
- 优点
synchronized使用在实例方法上有什么优点?
代码量减少,节俭了。
如果共享的对象就是this,并且需要同步的代码块就是整个方法体,建议使用这个方式。
根据上面的说法,Account类可以做下面的修改:
package ThreadAccountSimulation3;
/*
银行账户
不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
*/
public class Account {
private String actno;//账户
private double balance;//余额
public Account() {
}
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;
}
//取款方法
//在实例方法上可以使用synchronized吗?可以的
/*
synchronized出现在实例方法上,一定锁的是this,不能是其他的对象了。
这种方式灵活性较差。
另外一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步
可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
synchronized使用在实例方法上有什么优点?
代码量减少,节俭了。
如果共享的对象就是this,并且需要同步的代码块就是整个方法体,建议使用这个方式。
*/
public synchronized void withdraw(double money){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
如果使用局部变量的话:
建议使用StringBulider
因为局部变量不存在线程安全问题,使用StringBuffer效率比较低
另外要注意:
ArrayList是非线程安全的,Vector是线程安全的
HashMap,HashSet是非线程安全的,HashTable是线程安全的
总结synchronized使用方法
- 第一种写法:同步代码块
灵活
synchornizd(共享对象){
//同步代码块
}
- 第二种写法:在实例方法上使用synchronized
表示对象的方法一定是this,并且同步代码块是整个方法体
-
在静态方法上使用synchronized
表示找类锁
类锁永远只有1把
就算创建了100个对象,类锁也永远只有一把
对象锁:一个对象一把锁;100个对象100把锁
类锁:100个对象也可能是1把锁。
类锁是为了保证静态变量的安全
关于synchronized的面试题
package exam1;
//面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//答:不需要。因为doOther方法没有synchronized
public class Exam01 {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if("t2".equals(Thread.currentThread().getName())){
mc.doOther();
}
}
}
class MyClass{
//synchronized出现在实例方法上,说明锁的是this
public synchronized void doSome(){
System.out.println("doSome begins!");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over!");
}
public void doOther(){
System.out.println("doOther begins");
System.out.println("doOther over");
}
}
上述代码的运行结果为:
doSome begins!
doOther begins
doOther over
doSome over!
我们可以得到下面的结论:当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
将上述的代码进行修改,此时输出的结果会是怎么样的呢?
package exam2;
//面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//答:需要。
public class Exam01 {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if("t2".equals(Thread.currentThread().getName())){
mc.doOther();
}
}
}
class MyClass{
//synchronized出现在实例方法上,说明锁的是this
public synchronized void doSome(){
System.out.println("doSome begins!");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over!");
}
public synchronized void doOther(){
System.out.println("doOther begins");
System.out.println("doOther over");
}
}
上述代码的运行结果:
doSome begins!
doSome over!
doOther begins
doOther over
在doOther方法也被synchronized关键字修饰之后就需要等待doSome方法结束才能进行doOther方法了。
我们继续做出下面的修改:
package exam4;
//面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//答:需要。因为静态方法是类锁,不管创建了几个对象,类锁只有一个。
public class Exam01 {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//这个睡眠的作用是为了保证t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if("t2".equals(Thread.currentThread().getName())){
mc.doOther();
}
}
}
class MyClass{
//synchronized出现在静态方法上,是类锁
public synchronized static void doSome(){
System.out.println("doSome begins!");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over!");
}
public synchronized static void doOther(){
System.out.println("doOther begins");
System.out.println("doOther over");
}
}
死锁现象
package deadLock;
/*
死锁代码
要求会写,只有会写才能在以后的开发中注意这件事
因为死锁很难调试
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
synchronized在开发中最好不要嵌套使用。不小心会造成死锁。
2.5、如何解决线程的安全问题
我们以后在开发中如何解决线程安全问题?
是一上来就线程同步,使用synchronized吗?
不是,synchronized会使程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低,用户体验差。在不得已的情况下再选择线程同步机制。
解决方法
- 第一种方案:尽量使用局部变量代替“实例变量和静态变量”
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,那么实例变量的内存就不共享了。(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)
- 第三种方案:如果不能使用局部变量,且实例变量也不能创建多个,那么此时就只能使用synchronized,线程同步机制
守护线程
3.1线程的分类
线 程 { 用 户 线 程 守 护 线 程 : 后 台 线 程 , 比 如 垃 圾 回 收 线 程 线程\begin{cases} 用户线程\\ 守护线程:后台线程,比如垃圾回收线程 \end{cases} 线程{用户线程守护线程:后台线程,比如垃圾回收线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中有代表性的就是:垃圾回收线程(守护线程)
3.2守护线程
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程
守护线程用在什么地方呢?
守护的目的是守护,所有的用户线程都结束了那么守护线程就结束了
守护线程的使用位置:
比如每天0点时,系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在看护着,每到00:00的时候就备份一次,所有的用户线程都结束了,守护线程自动退出,没有必要进行数据备份了。
3.3守护线程的模拟
首先我们设置一个死循环线程:
package daemonThread;
/*
守护线程
*/
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据线程");
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 BakDataThread extends Thread{
public void run(){
int i = 0;
while (true){
System.out.println(Thread.currentThread().getName() + "---->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在没有设置守护线程的时候,main线程结束了,分支线程仍然会执行。
下面我们将该分支线程(备份线程)设置为守护线程,设置为守护线程只需要使用线程对象的setDaemon(true)
方法即可,下面是完整代码:
package daemonThread;
/*
守护线程
*/
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
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 BakDataThread extends Thread{
public void run(){
int i = 0;
//即使是死循环,由于该线程是守护者,当用户线程结束,守护线程自动终止
while (true){
System.out.println(Thread.currentThread().getName() + "---->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代码的运行结果为:
main---->0
备份数据线程---->1
备份数据线程---->2
main---->1
main---->2
备份数据线程---->3
main---->3
备份数据线程---->4
main---->4
备份数据线程---->5
main---->5
备份数据线程---->6
main---->6
备份数据线程---->7
main---->7
备份数据线程---->8
备份数据线程---->9
main---->8
main---->9
备份数据线程---->10
备份数据线程---->11
Process finished with exit code 0
可以看到,当用户线程(主线程)结束之后,后台线程——守护线程BakDataThread也自动结束。
这即是守护线程。
定时器
4.1定时器的作用
间隔特定的时间,执行特定的程序。
比如:
每周进行银行账号的总账操作。
每天进行数据的备份操作。
4.2定时器的实现方式
在实际的开发中,每隔一段时间执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种方式实现:
- 使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这是最原始的定时器。(比较low)
- 在Java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少使用,因为现在有很多高级框架都是支持定时任务的。
- 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
- SpringTask底层的原理就是Java.util.Timer
package daemonThread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
使用定时器执行指定任务
*/
public class TimerTest {
public static void main(String[] args) {
//创建定时器对象
Timer timer = new Timer();
//Timer t = new Timer(true);//守护线程的方式
//timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = null;
try {
firstTime = sdf.parse("2021-3-7 22:29:30");//返回值即是一个日期类型
} catch (ParseException e) {
e.printStackTrace();
}
timer.schedule(new LogTimerTask() ,firstTime, 1000*10);
}
}
//编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run() {
//在这里编写你要执行的任务即可
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String now = sdf.format(new Date());
System.out.println(now + ":成功完成了一次数据备份!");
}
}
要实现定时的任务依靠的是Time类中的schedule方法,方法中要有三个参数:
timer.schedule(定时任务,第一次执行时间,间隔多久执行一次)
其中的定时任务是一个TimerTask,这是一个抽象类,实现了Runnable接口,可以认为是一个线程。抽象类不能直接new对象,需要我们具体化,下面编写的LogTimerTask类就实现了这个定时任务的编写。在run方法中实现即可。
上述代码的运行结果为:
2021-03-07 22:29:30:成功完成了一次数据备份!
2021-03-07 22:29:40:成功完成了一次数据备份!
2021-03-07 22:29:50:成功完成了一次数据备份!
2021-03-07 22:30:00:成功完成了一次数据备份!
2021-03-07 22:30:10:成功完成了一次数据备份!
Process finished with exit code -1
实现线程的第三种方式:Callable接口
这是JDK8的新特性
这种方式实现的线程,可以获取线程的返回值,之前的两种方式是无法获取线程的返回值的。
思考:
系统委派线程去执行一个任务,该线程执行完之后可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
方法:
使用第三种方式:实现Callable接口方式。
实现线程的第三种方式:
实现Callable接口
这种方式的优点:可以获取到线程的执行结果
这种方式的缺点:效率比较低,在获取t线程的执行结果的时候,当前线程受阻塞,效率较低。
package theThirdMethord;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
实现线程的第三种方式:
实现Callable接口
这种方式的优点:可以获取到线程的执行结果
这种方式的缺点:效率比较低,在获取t线程的执行结果的时候,当前线程受阻塞,效率较低。
*/
public class ThreadTest15 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个“未来任务类”对象(采用匿名内部类)
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call方法相当于run方法。只不过这个有返回值
//线程执行一个任务,执行之后可能会有一个结果
//模拟执行
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method end");
int a = 100;
int b = 200;
return a + b;//自动装箱,变成Integer
}
});
Thread t = new Thread(task);//创建线程对象
//启动线程
t.start();
//这里是主线程main方法
//在主线程中,怎么获取t线程的返回结果?
//get方法的执行会导致"当前线程"的阻塞
Object obj = task.get();
System.out.println("线程的执行结果" + obj);
//main方法的执行必须要等待get()方法的结束
//而get()方法可能需要很久。因为get方法是拿另一个线程的执行结果
//而另一个线程的执行是需要时间的
System.out.println("hello world!");
}
}
关于Object类中的wait和notify方法(生产者和消费者模式)
5.1概述
1.wait和notify方法不是线程对象的方法,java中的任何一个java对象都有的方法,因为这两个方法是object类中自带的。
wait方法和notify方法不是通过线程对象调用。
不是这样的:t.wait();也不是这样的:t.notify()
5.2.wait()方法作用
表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait()方法的调用会让“当前线程”进入等待状态
3.notify方法作用
Object o = new Object();
o.notify();
表示:
唤醒两个正在o对象上等待的线程。
还有一个notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
5.3生产者和消费者模式
仓库中会有增加和删除的多线程操作,为了保证安全,所以wait和notify方法要建立在synchronized的基础之上。
-
使用wait方法和notify方法 实现“生产者和消费者”模式
什么是生产者和消费者模式?
生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法 -
wait方法和notify方法不是线程对象特有的方法,是普通java对象都有的方法
-
wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。
-
wait方法的作用:o.wait()让正在o对象上获取的线程进入等待状态,并且释放掉t线程之前占有的o对象的锁。
-
notify方法的作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放之前o对象上占有的锁
-
模拟这样的需求:
仓库采用List集合。
List集合中假设只能存储1个元素。
1个元素就表示仓库满了
如果List集合中元素是0,那么就表示仓库空了。
保证List集合中永远都是最多存储一个元素。
必须做到这个效果:生产一个消费一个
package theThirdMethord;
import java.util.ArrayList;
import java.util.List;
public class ThreadTest16 {
public static void main(String[] args) {
//创建一个仓库对象
List list = new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1 = new Thread(new Producer(list));
t1.setName("生产者线程");
//消费者线程
Thread t2 = new Thread(new Consumer(list));
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
//生产线程
class Producer implements Runnable{
//仓库
private List list;
public Producer(List list){
this.list = list;
}
@Override
public void run() {
//一直生产(使用死循环来模拟一直生产)
while (true){
synchronized (list){
if(list.size()>0){//大于零说明仓库中已经有
//当前线程进入等待状态并且释放list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能执行到这里说明什么:仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
//唤醒消费者消费
list.notify();
}
}
}
}
//消费线程
class Consumer implements Runnable{
//仓库
private 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.notify();
}
}
}
}
输出结果:
生产者线程--->java.lang.Object@34073e55
消费者线程--->java.lang.Object@34073e55
生产者线程--->java.lang.Object@3af371ac
消费者线程--->java.lang.Object@3af371ac
生产者线程--->java.lang.Object@2001f537
消费者线程--->java.lang.Object@2001f537
生产者线程--->java.lang.Object@3a88d7fe
消费者线程--->java.lang.Object@3a88d7fe
生产者线程--->java.lang.Object@3770a36c
消费者线程--->java.lang.Object@3770a36c
生产者线程--->java.lang.Object@16afda3f
消费者线程--->java.lang.Object@16afda3f
生产者线程--->java.lang.Object@39283d90
消费者线程--->java.lang.Object@39283d90
生产者线程--->java.lang.Object@45e32a6c
消费者线程--->java.lang.Object@45e32a6c
生产者线程--->java.lang.Object@77ba9a7f
习题:
ed (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.notify();
}
}
}
}
输出结果:
```java
生产者线程--->java.lang.Object@34073e55
消费者线程--->java.lang.Object@34073e55
生产者线程--->java.lang.Object@3af371ac
消费者线程--->java.lang.Object@3af371ac
生产者线程--->java.lang.Object@2001f537
消费者线程--->java.lang.Object@2001f537
生产者线程--->java.lang.Object@3a88d7fe
消费者线程--->java.lang.Object@3a88d7fe
生产者线程--->java.lang.Object@3770a36c
消费者线程--->java.lang.Object@3770a36c
生产者线程--->java.lang.Object@16afda3f
消费者线程--->java.lang.Object@16afda3f
生产者线程--->java.lang.Object@39283d90
消费者线程--->java.lang.Object@39283d90
生产者线程--->java.lang.Object@45e32a6c
消费者线程--->java.lang.Object@45e32a6c
生产者线程--->java.lang.Object@77ba9a7f
习题: