多线程(四):线程的同步


1.经典线程同步例子——卖票

理想状态下卖票
在这里插入图片描述
极端状态下卖票

在这里插入图片描述

使用实现Runnable接口的方式卖票

例子:使用实现Runnable接口的方式,创建三个窗口卖票,总票数为100张。

1.问题:卖票过程中,出现了重票,错票–>出现了线程的安全问题。
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来.直到线程a操作完ticket时, 其他线程才可以开始操作ticket.这种情况即使线程a出现了阻塞,也不能改变。

在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块

 synchronized(同步监视器){
     //需要被同步的代码
 }

说明:
1.操作共享数据的代码,即为需要被同步的代码.
2.共享数据:多个线程共同操作的变量.比如:ticket就是共享数据.
3.同步监视器,俗称:锁. 任何一个类的对象,都可以充当锁.
要求:多个线程必须要共用同一把锁.
补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器.

/**
 * 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
 *
 * 1.问题:卖票过程中,出现了重票,错票-->出现了线程的安全问题
 * 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票.
 * 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来.直到线程a操作完ticket时,
 *           其他线程才可以开始操作ticket.这种情况即使线程a出现了阻塞,也不能改变.
 * 4.在Java中,我们通过同步机制,来解决线程的安全问题.
 *   方式一:同步代码块
 *
 *   synchronized(同步监视器){
 *       //需要被同步的代码
 *   }
 *   说明:1.操作共享数据的代码,即为需要被同步的代码
 *       2.共享数据:多个线程共同操作的变量.比如:ticket就是共享数据.
 *       3.同步监视器,俗称:锁. 任何一个类的对象,都可以充当锁.
 *         要求:多个线程必须要共用同一把锁.
 *
 *         补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器.
 *   方式二:同步方法
 *
 *
 *  5.同步代码块的方式,解决了线程安全的问题.---好处
 *    操作同步代码时,只能有一个线程参与,其他线程等待.相当于一个单线程的过程,效率低.---局限性.
 */
class  Window implements  Runnable{
    private  int ticket=100;
    //Object obj=new Object();
    @Override
    public void run() {
        while(true){
            synchronized(this){  //同步此时的this:唯一的Window对象
                if (ticket>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }

        }

    }
}

public  class  TestRunnable{
    public static void main(String[] args) {
         Window w=new Window();
         Thread t1=new Thread(w);
         Thread t2=new Thread(w);
         Thread t3=new Thread(w);
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");

         t1.start();
         t2.start();
         t3.start();

    }
}

方式二:同步方法

使用同步方法来解决实现Runnable接口的线程安全问题。
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明.
2.非静态的同步方法,同步监视器是:this
3.静态的同步方法,同步监视器是:当前类本身


/**
 * 使用同步方法来解决实现Runnable接口的线程安全问题
 * 1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明.
 * 2.非静态的同步方法,同步监视器是:this
 *   静态的同步方法,同步监视器是:当前类本身
 */

class  Window implements  Runnable{
    private  int ticket=100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private synchronized void show(){ //同步监视器:this
            if (ticket>0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
                ticket--;
            }

    }
}

public  class  TestRunnable{
    public static void main(String[] args) {
         Window w=new Window();
         Thread t1=new Thread(w);
         Thread t2=new Thread(w);
         Thread t3=new Thread(w);
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");

         t1.start();
         t2.start();
         t3.start();

    }
}

使用继承Thread类的方式卖票

例子:使用继承Thread类的方式,创建三个窗口卖票,总票数为100张。

方式一:同步代码块


/**
 * 使用同步代码块解决继承Thread类的方式的线程安全问题。
 * 例子:创建三个窗口卖票,总票数为100张,使用实继承Thread类的方式
 *
 *说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
 */


class Windows2 extends  Thread{
    private  static  int ticket=100;
    private  static  Object obj=new Object();  //静态锁,共用一把锁

    @Override
    public void run() {
        while (true){
            //正确的
            //synchronized (obj){
            synchronized (Windows2.class){  //类是对象,类唯一。类锁
                if (ticket>0){
                    try{
                        Thread.sleep(100);
                        System.out.println(getName()+":卖票,票号为:"+ticket);
                        ticket--;
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class TestThread {

    public static void main(String[] args) {
        Windows2 t1=new Windows2();
        Windows2 t2=new Windows2();
        Windows2 t3=new Windows2();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

方式二:同步方法

/**
 * 使用同步方法解决继承Thread类的方式的线程安全问题。
 */


class Windows2 extends  Thread{
    private  static  int ticket=100;
    private  static  Object obj=new Object();  //静态,共用一把锁

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){  //同步监视器:Windows2.calss
        if (ticket > 0) {
            try {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TestThread {

    public static void main(String[] args) {
        Windows2 t1=new Windows2();
        Windows2 t2=new Windows2();
        Windows2 t3=new Windows2();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

2.线程安全的单例模式之懒汉式

/*
 *使用同步机制将单例模式的懒汉式改为线程安全的.
 *
 */
public class SingleTest {

}

class Single{

    private Single(){}

    private static  Single instance=null;

    public static  Single getInstance(){
        //效率更高
        if (instance==null){
            synchronized(Single.class){
                if (instance==null){
                    instance=new Single();
                }
            }
        }
        return instance;
    }

}

3.线程的死锁问题

  • 死锁
  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
  • 解决办法
  • 专门的算法、原则。
  • 尽量减少同步资源的定义。
  • 尽量避免嵌套同步。
  • 死锁例子

public class SuoTest {

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

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

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

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

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

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

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

4.解决线程安全问题的方式三:Lock锁

  • Lock锁
  • 从jdk5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,同拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
  • Lock锁解决线程安全问题例子

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解决线程安全问题的方式三:Lock锁  ---JDK5.0新增
 */

class  Window 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(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":买票,票号为"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }

}

public  class  TestRunnable{
    public static void main(String[] args) {
         Window w=new Window();
         Thread t1=new Thread(w);
         Thread t2=new Thread(w);
         Thread t3=new Thread(w);
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");

         t1.start();
         t2.start();
         t3.start();

    }
}
  • synchronized与Lock的异同?

相同: 二者都可以解决线程安全问题。
不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock需要手动的启动同步lock(),同时结束同步也需要手动的实现unlock()。

5.练习题

银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?

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

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

class Customer extends  Thread{
    private  Account acct;
    public  Customer(Account acct){
        this.acct=acct;
    }

    @Override
    public void run() {
        for(int i=0;i<3;i++){
            acct.deposit(1000);
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        Account acct=new Account(0);
        Customer c1=new Customer(acct);
        Customer c2=new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();

    }

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值