Java基础整理(二十)

多线程(下)

传送门:多线程(上)

11. 线程同步

(1)线程安全

其实,在以后的开发中,所有的项目都会运行在服务器上,而服务器已经将线程的定义,线程对象的创建,线程的启动等都实现完了。而我们更需要关注的是在多线程并发的环境下,数据的安全性!

  • 这里拿两个人同时去银行对一个用户取钱为例

在这里插入图片描述

  • 什么时候数据在多线程并发的环境下会存在安全问题呢?

    • 条件一:多线程并发
    • 条件二:有共享数据
    • 条件三:共享数据存在修改行为

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

  • 如何解决线程安全问题呢?

    使用“线程同步机制”,线程同步就是线程排队了,不能并发了,所以会牺牲一部分效率,但是安全更重要

  • 线程同步的两个基本术语

    • 异步编程模型:线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1。谁也不需要等谁,多线程并发。
    • 同步编程模型:线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,效率较低,线程排队执行

(2)线程安全问题实例

银行账户:不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题

Test类

public class Test {
    public static void main(String[] args) {

        Account account = new Account("act-001",10000);
        Thread t1 = new Thread(new AccountThread(account,5000));
        Thread t2 = new Thread(new AccountThread(account,5000));
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

Account类

public class Account {

    private String id;
    private double balance;

    public Account() {
    }

    public Account(String id, double balance) {
        this.id = id;
        this.balance = balance;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public void withDraw(double money){
        //t1和t2并发执行这个方法(两个栈操作堆中的同一个对象)
        double before = balance;
        double after = balance - money;
        //模拟网络延迟
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
        balance = after;
    }
}

AccountThread类

public class AccountThread implements Runnable{

    //两个线程必须共享同一个对象
    private Account act;
    private double money;

    public AccountThread(Account act, double money) {
        this.act = act;
        this.money = money;
    }

    @Override
    public void run() {
        act.withDraw(money);
        System.out.println(Thread.currentThread().getName()+"对账户"+act.getId()+"取款"+money+"成功,余额为"+act.getBalance());

    }
}

输出结果:在这里插入图片描述

显然,从输出结果上来看就出现了错误。两个用户同时取款5000,最后余额应为0,而结果还剩5000。


(3)利用线程同步解决(2)问题

线程同步机制的语法:

//一个线程把这里的代码全部执行结束,另一个线程才能进来,必须线程排队,不能并发
synchronized(){
    //线程同步代码块
}

()中写的要看你想让哪些线程同步。假设t1,t2,t3,t4,t5有5个线程。只希望t1,t2,t3线程排队,t4,t5不需要排队。这时,要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4,t5来说是不共享的。

  • 对(2)进行线程同步,只需要对withDraw方法进行修改

    public void withDraw(double money){
        //账户对象是共享的,那么this就是账户对象
        //不一定是this,这里只要是多线程共享的那个对象就行
            synchronized (this) {
                double before = balance;
                double after = balance - money;
                //模拟网络延迟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
                balance = after;
            }
        }
    
  • 分析

    public void withDraw(double money){
        Object obj = new Object()
            synchronized (obj) {
                double before = balance;
                double after = balance - money;
                //模拟网络延迟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
                balance = after;
            }
        }
    

    将obj写入到括号内,这样可以吗?

    不可以,因为obj不是共享对象,是在withDraw方法内的局部变量,每一个线程来执行该方法时,都会创建一个新的Object对象

  • 分析

    public void withDraw(double money){
            synchronized ("abc") {
                double before = balance;
                double after = balance - money;
                //模拟网络延迟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
                balance = after;
            }
        }
    

    将字符串“abc”写入到括号内,可以吗?

    可以,因为“abc”在字符串常量池中,所有线程共享,这样写的话就是所有线程都同步

  • 分析

    public synchronized void withDraw(double money){
                double before = balance;
                double after = balance - money;
                //模拟网络延迟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
                balance = after;
        }
    

    直接将synchronized写在实例方法上,一定锁的是this,不能是其他对象。

    这种方式不灵活,还有可能扩大同步范围,不推荐使用

  • 总结

    Java中三大变量

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

    以上三大变量中,局部变量永远不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中。实例变量在堆中,堆只有1个。静态变量在方法区中,方法区只有一个。堆和方法区都是多线程共享的,所以可能存在安全问题

  • 在StringBuilder和StringBuffer之间选择时,如果使用局部变量的话,可以选择用StringBuild,因为局部变量不存在线程安全问题,使用带有同步机制的StringBuffer效率比较低

    ArrayList是非线程安全的,Vector是线程安全的

    HashMap、HashSet是非线程安全的、HashTable是线程安全的


(4)synchronized三种写法总结
  • 第一种:同步代码块

    synchronized(线程共享对象){
        //同步代码块
    }
    
  • 第二种:实例方法上使用synchronized,表示共享对象一定是this,并且同步代码块是整个方法体

  • 第三种:静态方法上使用synchronized,表示找类锁,类锁永远只有一把,创建多少个对象都是一把类锁


12. 死锁

  • 概念:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去;此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

  • 代码实现

    public class DeadLock {
        public static void main(String[] args) {
    
            Object o1 = new Object();
            Object o2 = new Object();
    
            //两个线程共享o1,o2
            Thread t1 = new MyThread1(o1,o2);
            Thread t2 = new MyThread2(o1,o2);
    
            t1.setName("t1");
            t2.setName("t2");
    
            t1.start();
            t2.start();
    
        }
    }
    
    class MyThread1 extends Thread{
    
        Object o1;
        Object o2;
    
        public 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;
    
        public 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)){
    
                }
            }
        }
    }
    

    所以,synchronized在开发中最好不要嵌套使用,有可能不小心就会出现死锁


13. 如何解决线程安全问题

开发中应该怎么解决线程安全问题呢?直接使用synchronized可以吗?

最好不要,因为synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低

  • 第一种方案:

    尽量使用局部变量代替“实例变量和静态变量”

  • 第二种方案:

    如果必须是实例变量,那么可以创建多个对象,这样实例变量的内存就不共享了,就没有数据安全问题了

  • 第三种方案:

    如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synochronized,线程同步机制


14. 守护线程

  • Java语言中线程分为两大类:用户线程和守护线程,典型的用户线程就是main方法,守护线程就是垃圾回收线程

  • 特点

    一般守护线程是一个死循环,所有的用户线程只要结束,守护线程就会自动结束

  • 守护线程用在什么地方呢?

    例如每天在固定时间进行备份,就可以使用定时器,并设置成守护线程。每到那个时间就备份一次,所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了

  • 语法

    线程对象.setDaemon(true);
    
  • 示例

    public class Test {
        public static void main(String[] args) {
    
            Thread t = new MyThread();
            t.setName("t1");
            //在启动线程之前,设置为守护线程
            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();
                }
            }
        }
    }
    

15. 定时器

  • 作用

    间隔特定的时间,执行特点的程序

  • 在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的。在Java中也有多种实现方式

    • 可以使用sleep方法,设置睡眠时间,每到这个时间点就醒来执行程序,但是不推荐这么使用
    • 在java类库中有已经写好的定时器:java.util.Timer,可以直接用
    • 但是在实际开发中,更常用的是框架来进行配置定时器任务,但是底层实现原理还是Timer
  • 实例

    public class Test {
        public static void main(String[] args) throws ParseException {
    
         //创建定时器对象
         Timer timer = new Timer();
         //Timer timer = new Timer(true); 设置成守护线程
    
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         Date firstIme = sdf.parse("2021-12-20 21:13:00");
         timer.schedule(new LogTimerTask(),firstIme,1000*10);
    
    
        }
    }
    
    //编写一个定时任务
    //假设这是一个记录日志的定时任务
    class LogTimerTask extends TimerTask{
    
        @Override
        public void run() {
            //编写索要执行的任务
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String strTime = sdf.format(new Date());
            System.out.println(strTime+"完成了一次数据备份");
        }
    }
    

    输出结果:在这里插入图片描述

    TimerTask是一个抽象类,编写定时任务类时要继承TimerTask并重写其中的run方法

    timer.schedule(定时任务,第一次执行时间,间隔时间)

  • 上面例子也可以采用匿名内部类

    public class Test {
        public static void main(String[] args) throws ParseException {
    
         Timer timer = new Timer();
    
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         Date firstIme = sdf.parse("2021-12-20 21:13:00");
            //匿名内部类
         timer.schedule(new TimerTask() {
             @Override
             public void run() {
                 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                 String strTime = sdf.format(new Date());
                 System.out.println(strTime+"完成了一次数据备份");
             }
         }, firstIme, 1000 * 10);
        }
    }
    

16. wait和notify

在这里插入图片描述

  • wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个方法是Object类中自带的,不是通过线程对象调用的

  • wait()方法的作用?

    Object o = new Object();
    o.wait();
    

    让正在o对象上活动的线程(当前线程)进入等待状态,无限期等待,直到被唤醒为止,并且会同时释放t线程之前占有o对象的锁(详细看17中的例子)

  • notify()方法的作用?

    Object o = new Object();
    o.notify();
    

    唤醒正在o对象上等待的线程,还有一个notifyAll()方法唤醒o对象上处于等待的所有线程,不会释放o对象之前占有的锁(详细看17中的例子)


17. 生产者与消费者模式

在这里插入图片描述

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

需求:仓库使用List集合,List集合中假设只能存储一个元素,一个元素就表示仓库满了,如果List集合中元素个数为0,就表示仓库空了。保证List集合永远都是最多存储一个元素

public class Test {
    public static void main(String[] args) {

        //创建一个共享仓库对象
        List list = new ArrayList();
        //创建两个线程对象
        Thread pro = new Thread(new Producer(list));
        Thread con = new Thread(new Consumer(list));

        pro.setName("Producer");
        con.setName("Consumer");

        pro.start();
        con.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){
                    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{

    //仓库
    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.notifyAll();
            }
        }

    }
}

运行结果:在这里插入图片描述

  • 为什么调用wait方法后会释放锁?

    因为调用wait方法后,当前线程已经进入了等待状态,如果再不释放锁的话,可能会导致死锁的发生

  • 那调用notify方法后,当前线程还会继续抢锁吗?

    会继续抢锁,但并没有关系,因为即使当前线程真的抢到了锁,但是会因为条件不满足,而调用wait方法进入等待状态,而wait方法会释放锁,这时另一个线程就又得到了拿到锁的机会

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值