Java线程安全问题的三种解决方式

新的一天,回顾一下昨天的问题!

首先从问题引入:
有三个窗口共同卖100张票,使用实现Runnable接口的方式
问题:在买票的过程中,出现了重票、错票。
原因:当一个线程在操作ticket过程中,操作尚未完成,其他线程参与进来,也操作车票---->出现了线程安全问题。
如何解决:当一个线程a操作ticket时,其他线程不能参与进来,直到a操作完ticket时,其他线程线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
这就涉及到同步机制来解决线程安全问题:

  1. 同步代码块
  2. 同步方法
  3. Lock锁

弊端: 操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。

一.同步代码块:

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

  1. 操作共享数据的代码,即为需要被同步的代码 -->不能包裹多了,也不能包裹少了
  2. 共享数据:多个线程共同操作的变量。比如: ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。但使用时要特别小心。

要求:多个线程必须要共用同一把锁。

同步代码块解决买票问题

public class Test{
    public static void main(String[] args) {
        Window2 w = new Window2();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window2 implements Runnable {
    private int ticket = 100;
    //private final Object obj = new Object();
    @Override
    public void run() {
            while (true){
                synchronized (this){//synchronized (obj){
                    if(ticket>0){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);
                        ticket--;
                    }else {
                        break;
                    }
                }
            }
    }
}

这里提一下代码块包裹多与少的问题,如果将while(true){ }也包裹进去,那么一个线程就将票卖完了,因为只有当代码块中的代码执行完毕,线程才会放开锁

二.同步方法:

结合同步代码块的理解,如果将操作共享数据的代码完整的声明在一个方法中,可以将方法声明为同步----加上synchronized关键字
总结

  1. 同步方法仍然涉及到同步监视器,只是不需要显示的声明;
  2. 非静态同步方法,同步监视器为this;
    静态的同步方法,同步监视器为 当前类本身,当一个类第一次加载的时候就唯一存在于内存中,通过xxx.class获得,这也是一个对象
三.创建多线程时-同步方法的同步监视器的补充说明
  1. 在实现Runable接口的方式中可以用this充当同步监视器
  2. 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可能不小心就创建了多个继承类,导致this不唯一。可以考虑(Xxx.class),这也是一个对象
  3. 关于第二条慎用的理解(不是说不用),请看下面代码----两个储户存一个账户。操作共享数据是Account类,作为Customer类的成员,能保证唯一性。
class Account{
    private int balance;
    public Account(int balance){
        this.balance = balance;
    }

    public synchronized void save(int money){
        if(money > 0){
            balance += money;
            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.save(1000);
        }
    }
}

public class Test{
    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();
    }
}
四.Lock锁:

JDK5.0新增的解决办法——通过显式定义同步锁来实现同步,锁由Lock对象充当,使用继承类ReentrantLock。具体步骤看代码注释。
问题: synchronized与Lock的异同?
相同:二者都可以解中线程安全问题
不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(Lock()) ,同时结束同步也需要手动的实现(unLock() )

Lock锁解决买票问题

import java.util.concurrent.locks.ReentrantLock;

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

public class Lock {
    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();

    }
}

五.小结一下锁的释放问题

释放锁的操作
●当前线程的同步方法、同步代码块执行结束。
●当前线程在同步代码块、同步方法中遇到break、return终止 了该代码块、该方法的继续执行。
●当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
●当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
●线程执行同步代码块或同步方法时,程序调用Thread .sleep()、Thread.yield()方法暂停当前线程的执行。
●线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
➢应尽量避免使用suspend()和resume()来控制线程(过时的)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值