线程的安全问题(四)

本文探讨了线程安全的概念,通过银行取款的例子解释了线程并发不安全的原因,并介绍了如何通过Synchronized关键字和Lock实现线程同步,确保数据的安全性。此外,还讨论了线程安全的补充内容,包括锁的释放、不会释放锁的操作、死锁问题以及Synchronized与Lock的对比。
摘要由CSDN通过智能技术生成

线程的安全问题

第四节 线程的安全问题



前言

相信大家在学习java基础的过程中,总会听到一些方法涉及线程的安全与否的问题,到底什么是线程的安全呢?接下来将会针对这个问题来进行解答。


一、线程安全是什么?

假设你在工行有一个银行账户,两张银联卡(自己手里一张,女朋友手里一张),里面有100万。假设取钱就两个过程:1.检查账户余额,2.取出现金(如果要取出的金额 > 账户余额,则取现成功,否则取现失败)。有一天你要买房想把钱取出来,而此时你女朋友也想买一辆车(假设你们事先没有商量)。两个人都在取钱,你在A号ATM机取100万,女朋友在B号ATM机取80万。这时A号ATM检查账户余额发现有100万,可以取出;而与此同时,同一时刻B号ATM也在检查账户余额发现有100万,可以取出;这样,A、B都把钱取出来了。

100万的存款取出180万,银行就出现问题了。这就是线程并发的不安全性。为避免这种情况发生,我们要将多个线程对同一数据的访问同步,确保线程安全。

所谓同步(synchronization)就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。同步最常见的方式就是使用锁(Lock),也称为线程锁。锁是一种非强制机制,每一个线程在访问数据或资源之前,首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁被占用时试图获取锁,线程会进入等待状态,直到锁被释放再次变为可用。这样就可以保证线程对共享数据从之前的并行操作改为串行操作,这样就保证了数据的安全性了。

在极端情况下,即你和女朋友同时取钱,银行认为你们的操作都是没有问题的时候,银行将会出现错误的处理结果。

  1. 问题的原因:
    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
  2. 解决办法:
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
    在这里插入图片描述

二、线程安全的实现方式

需求:实现一个售票的功能,要求3个窗口同时对100张票进行售卖,不得出现错票。
分析:

  1. 三个窗口,即三个线程,所以这是一个多线程程序。
  2. 一共有100张票,即这三个线程共享这100张票,所以存在线程安全问题,需要进行安全处理。
    在这里插入图片描述
    在这里插入图片描述

一、Synchronized关键字

1.同步代码块实现

  1. 格式:
synchronized (同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
						要求:多个线程必须要共用同一把锁。){
	// 需要被同步的代码;
}
  1. 实现需求代码
class ThreadTest1 implements Runnable{
    //定义共享属性,票
    private int ticket = 100;
    //定义同步监视器
    //同步监视器对象的选用很关键。要选择线程共享的对象,比如下面例子的 threadTest1, 它是static修饰的才行,
    // 如果没有static修饰,则是使用不同的同步监视器(不是同一个对象),相当于是两把钥匙。
    static ThreadTest1 threadTest1 = new ThreadTest1();
    @Override
    public void run() {
        while (true){
            //定义同步代码块
            synchronized (threadTest1){
                if (ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
//测试
public class ThreadSynTest1{
    public static void main(String[] args) {
        ThreadTest1 threadTest1 = new ThreadTest1();
        Thread t1 = new Thread(threadTest1);
        Thread t2 = new Thread(threadTest1);
        Thread t3 = new Thread(threadTest1);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

2.同步方法实现

synchronized还可以放在方法声明中,表示整个方法为同步方法。

  1. 格式:
public synchronized void xxx(String name){
	// 需要被同步的代码;
}
  1. 实现需求代码
class ThreadTest2 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        //此处使得程序一直在运行,运行结束后请手动关闭
        while (true){
        	//调用同步方法
            show();
        }
    }
    //实现同步方法
    private synchronized void show(){
        if (ticket > 0){
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}
public class ThreadSynTest2 {
    public static void main(String[] args) {
        ThreadTest2 threadTest2 = new ThreadTest2();
        Thread t1 = new Thread(threadTest2);
        Thread t2 = new Thread(threadTest2);
        Thread t3 = new Thread(threadTest2);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

二、使用Lock(锁) JDK5.0新增

  1. 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  2. java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  3. ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
  4. 格式:
class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码;
        }
        finally{
            lock.unlock();
        }
    }
}
  1. 实现需求代码
class ThreadTest3 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){
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
public class ThreadLockTest3 {
    public static void main(String[] args) {
        ThreadTest3 threadTest3 = new ThreadTest3();
        Thread t1 = new Thread(threadTest3);
        Thread t2 = new Thread(threadTest3);
        Thread t3 = new Thread(threadTest3);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

三、线程安全的相关补充

一、释放锁的操作

  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

二、不会释放锁的操作

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程。

三、线程的死锁问题

当不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行。

四、synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值