Java多线程:Chapter 2线程的并发和安全

线程的调度

Java中提供的那些方法是和线程调度有关的呢?

实例方法:

  • void setpriority ( int newpriority) 设置线程的优先级

  • int getpriority() 获取线程的优先级

    最低优先级是1

    默认优先级是5

    最高优先级是10

    优先级比较高的获取CPU时间片可能会多一些(但也不完全是,大概率情况下是会高一些)

静态方法

static void yield()   让位方法

​ 暂停当前正在执行的线程对象,并执行其它线程

yield方法不是阻塞方法。让当前线程让位,让给其他的线程使用

​ yield方法会让当前线程从“运行状态”回到“就绪状态”

​ 注意:在回到就绪之后有可能会再次抢到

image-20210302112910356

实例方法:

​ 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什么情况下数据在多线程并发时会存在安全问题呢?

image-20210305113837548

三个条件:
  • 条件一:多线程并发

  • 条件二:有共享数据

  • 条件三:共享数据有修改的行为

满足以上三个条件之后就会存在线程安全问题

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把锁。一个对象一把锁

上述代码的执行原理是:

  1. 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先,一个后

  2. 假设t1先执行了,遇到了synchronized,这时候自动找后面对象的对象锁

    找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直是占有这把锁的。直到同步代码块代码结束,这把锁才会释放

  3. 假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有后面的共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序

这样就达到了线程的排队执行

这里要注意的是:这个对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。

Java中有三大变量:【重要问题】

  • 实例变量:在堆中
  • 静态变量:在方法区
  • 局部变量:在栈中

以上三大变量中:

​ 局部变量永远不会存在线程安全问题

​ 因为局部变量不共享。(一个线程一个栈)

​ 局部变量在栈中,所以局部变量永远不会共享

实例变量在堆中,堆只有一个

静态变量在方法区中,方法区只有一个

堆和方法区都是多线程共享的,所以可能存在线程安全问题

在AccountThread中做下面的修改也可以达到相同的目的:

image-20210305181910036

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生产者和消费者模式

image-20210309191900866

仓库中会有增加和删除的多线程操作,为了保证安全,所以wait和notify方法要建立在synchronized的基础之上。

  1. 使用wait方法和notify方法 实现“生产者和消费者”模式

    什么是生产者和消费者模式?
    生产线程负责生产,消费线程负责消费
    生产线程和消费线程要达到均衡
    这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法

  2. wait方法和notify方法不是线程对象特有的方法,是普通java对象都有的方法

  3. wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。

  4. wait方法的作用:o.wait()让正在o对象上获取的线程进入等待状态,并且释放掉t线程之前占有的o对象的锁。

  5. notify方法的作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放之前o对象上占有的锁

  6. 模拟这样的需求:
    仓库采用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

习题:

image-20210309205418763

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Blanche117

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值