多线程(二)(线程安全问题的解决、死锁问题、线程的通信等)

线程的同步

线程的生命周期如下图:
在这里插入图片描述

  1. 问题:如上一篇文章末尾提到的,在卖票过程中,出现了重票、错票–>出现了线程的安全问题
  2. 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票,导致了错票、乱票的情况
  3. 如何解決:当一个线程a在操作 ticket的时候,其他线程不能参与进来。直到线程a操作完 ticket时,其他线程才可以开始操作 ticket。这种情况即使线程a出现了阻塞,也不能被改变。
  4. 在Java中,我们通过同步机制,来解决线程的安全问题。
  5. 同步的方式,解决了线程的安全问题。----好处
    操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

  • 方式一:同步代码块

      synchronized(同步监视器){
      //需要被同步的代码
      }
      
      说明:
      1.操作共享数据的代码,即为需要被同步的代码 ---> 不能包含代码多了,也不能包含代码少了。
      2.共享数据:多个线程共同操作的変量。比如: ticket就是共享数据。
      3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
      	要求:多个线程必须要共用同一把锁。
    
    
      补充:在实现 Runnable接口的创建多线程的方式中,我们可以考虑使用this充当同步监视器
      说明:在继承 Thread类创建多线程的方式中。慎用this当同步监视器,考虑使用当前类充当同步监视器
    
  • 方式二:同步方法
    如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。

      关于同步方法的总结:
      1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
      2.非静态的同步方法,同步监视器是:this
      	静态的同步方法,同步监视器是:当前类本身
    
  • 方式三:Lock锁(jdk5.0新增)

      1.实例化ReentrantLock
      2.调用锁定方法lock()
      3.调用解锁方法:unlock()
    

重点!

		 synchronized与Lock的异同?
		 相同:二者都可以解决线程安全问题
		 不同:
		 synchronized机制在执行完相应的同步代码后,自动的释放同步监视器
		 Lock需要手动的启动同步(lock()),同时结束也需要手动的实现(unlock())
		 
		 优先使用顺序:
		 Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)





方式一:同步代码块

实现Runnable方法:
使用 this 或者 object一个唯一的对象 充当同步监视器

//使用同步代码块处理实现Runnable接口的方式中的线程安全问题
class Sell implements Runnable{
    private int ticket=100;
    Object object =new Object();
    @Override
    public void run() {
        while (true){
            synchronized(this){//this指的是当前的对象,因为sell只new了一次,所以唯一
                //正确    synchronized(object)
                if (ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
                    ticket--;
                }else {
                    System.out.println("没票啦");
                    break;
                }
            }
        }
    }
}

继承Thread类:
使用当前类充当同步监视器 (类名.class)

//使用同步代码块处理继承Thread类的方式中的线程安全问题
class Sell1 extends Thread{
    private static int ticket=100;
    static Object object =new Object();
    //Object object =new Object();不行
    @Override
    public void run() {
        while (true){
            synchronized (Sell1.class){    //说明类.class也是对象
                //正确  synchronized (object)
                //错误  synchronized (this)  因为是t1,t2,t3三个对象
                if (ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
                    ticket--;
                }else {
                    System.out.println("没票啦");
                    break;
                }
            }
        }
    }
}

方式二:同步方法

实现Runnable方法:
非静态的同步方法,同步监视器是:this

//使用同步方法处理实现Runnable接口的方式中的线程安全问题
class Sell2 implements Runnable{
    private int ticket=100;
    @Override
    public void run() {
        while (true){
            show();
        }
    }
    public synchronized void show(){
        if (ticket>0){
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
            ticket--;
        }
    }
}

继承Thread类:
静态的同步方法,同步监视器是:当前类本身

//使用同步方法处理继承Thread类的方式中的线程安全问题
class Sell3 extends Thread{
    private static int ticket=100;
    @Override
    public void run() {
        while (true){
            show();
        }
    }
    public static synchronized void show(){  //同步监视器t1,t2,t3
        if (ticket>0){
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
            ticket--;
        }
    }
}

方式三:Lock锁

实现Runnable方法:
private ReentrantLock lock=new ReentrantLock();

//使用Lock锁处理实现Runnable接口的方式中的线程安全问题
class Sell4 implements Runnable{
    private int ticket=100;
    //1.实例化ReentrantLock
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                //2.调用锁定方法lock()
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
                    ticket--;
                }else {
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}

继承Thread类:
private static ReentrantLock lock=new ReentrantLock();

//使用Lock锁处理继承Thread类的方式中的线程安全问题
class Sell5 extends Thread{
    private static int ticket=100;
    private static ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":当前的票号是:"+ticket);
                    ticket--;
                }else {
                    break;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}




小Demo:

		银行有一个账户。
		有两个储户分别向同一个账户存3000元,毎次存1000元,存3次。每次存完打印账户余额
		
		分析:
		1.是否是多线程问题?是,两个储户线程
		2.是否有共享数据?有,账户(或账户余额)
		3.是否有线程安全问题?有
		4.需要考虑如何解线程安全问题?同步机制:有三种方式。

代码如下:
其中balance是共享数据,所以存在线程安全问题,我们对其使用同步方法即可解决安全问题

public class AccountDemo {
    public static void main(String[] args) {
        Account account=new Account(0);
        Customer c1=new Customer(account);
        Customer c2=new Customer(account);
        c1.start();
        c2.start();
    }
}
class Account{
    private double balance;

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

    //存钱
    public synchronized void deposit(double atm){
        if (atm>0){
            balance += atm;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"存钱成功。余额为"+balance);
        }
    }
}
class Customer extends Thread{
    private Account account;

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}



synchronized关键字的底层原理:
① synchronized 同步语句块的情况
通过 JDK ⾃带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息。
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执⾏
monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象
头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏
monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞
等待,直到锁被另外⼀个线程释放为⽌。

② synchronized 修饰⽅法的的情况
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问
标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

单例模式的懒汉式改写为线程安全的 地址


线程的死锁

死锁问题:

  1. 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃白已需要的同步资源,就形成了线程的死锁
  2. 说明:
    (1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
    (2)我们使用同步时,要避免出现死锁

死锁产生的四个条件:
①互斥条件:该资源任意一个时刻只由一个线程占用
②请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
③不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
④循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

如何避免死锁:
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资
    源需要互斥访问)。
  2. 破坏请求与保持条件 :⼀次性申请所有的资源。
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放
    它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破
    坏循环等待条件。

死锁的Demo:
这是一个很经典的死锁的demo,当s2休息,下边的s1也在休息,并未释放自己,当两个同时结束s2想找s1,s1想找s2时,发现找不到对方,产生死锁。

public class DeadLine {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("1");
                    s2.append("a");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized(s1){
                        s1.append("2");
                        s2.append("b");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("3");
                    s2.append("c");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized(s2){
                        s1.append("4");
                        s2.append("d");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

线程的通信

线程通信的例子:使用两个线程打印1-100.线程1,线程2 交替打印

涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  • notify():一旦执行此方法,就会唤醒被wait的ー个线程。如果有多个线程被wait,就唤醒优先级高的线程
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的ー个线程。

说明:

  1. wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中
  2. wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器
    否则,会出现IllegalMonitorStateException异常
  3. wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中

面试题:
sleep() 和 wait()的异同?
4. 相同点:一旦执行此方法,都可以使得当前的线程进入阻塞状态。
5. 不同点:
(1)sleep是线程中的方法,wait是object类中的方法
(2)两个方法声明的位置不同: Thread类中声明 sleep(), Object类中声明wait()
(3)调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中
(4)关于是否释放同步监器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait会释放锁()




小Demo:

public class waitandnotify {
    public static void main(String[] args) {
        number1 n=new number1();
        Thread t1=new Thread(n);
        Thread t2=new Thread(n);
        t1.start();
        t2.start();
    }
}
class number1 implements Runnable{
    private int number=0;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                //进入锁以后唤醒其他的阻塞状态的
                notifyAll();
                if (number<100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        //打印完就wait,进入了阻塞状态,其他的就绪状态的继续打印过程
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }
    }
}

线程通信的应用:经典例子:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停ー下;如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品

分析:

  1. 是否是多线程问题?是,生产者线程,消费者线程
  2. 是否有共享数据?是,店员(或产品)
  3. 如何解决线程的安全问题?同步机制,有三种方法
  4. 是否涉及线程的通信?是
public class product {
    public static void main(String[] args) {
        Clerk c=new Clerk();
        Producer p=new Producer(c);
        Consumer cm=new Consumer(c);
        p.setName("生产者");
        cm.setName("消费者");
        p.start();
        cm.start();

    }
}
class Clerk{
    private int  productCount=0;
    public synchronized void produceConsumer() {
        if (productCount>0){
            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个商品");
            productCount--;
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void produceProducer() {
        if (productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个商品");
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Producer extends Thread{//生产者
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+" :开始消费产品....");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProducer();
        }
    }
}
class Consumer extends Thread{//生产者
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+" :开始生产产品....");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceConsumer();
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值