Java 多线程(二)——线程安全

一、前言

多线程的实现方式 我们已经讲完了,今天我们来讲线程安全

二、线程安全

2.1、线程安全概述

如果有多个线程在同时运行,而这些线程可能会同时访问某一共享变量,这样就会产生线程安全问题。

我们通过一个卖票案例来演示线程安全问题:

public class RunnableImpl implements Runnable {

    // 定义一个多线程共享的票源
    private int ticket = 100;

    // 卖票
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

测试类:

public class TestSellTicket {

    // 创建三个线程,同时开启,对共享的票进行出售
    public static void main(String[] args) {
        // 创建Runnable接口的实现类
        RunnableImpl runnable = new RunnableImpl();
        // 创建Thread
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t0.start();
        t1.start();
        t2.start();
    }
}

测试结果如下所示:

结果

可以看到运行结果中出现了重复的票,这就是线程不安全。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能出现线程安全的问题。

2.2、线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java 中提供了同步机制 (synchronized) 来解决。

根据上述卖票案例简述:

窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作结束,窗口 1 和窗口 2 和窗口 3 才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺 CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制。主要有以下三种方式:

  1. 同步代码块;
  2. 同步方法;
  3. 锁机制;

2.2.1、同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
	需要同步操作的代码
}

注意:

  1. 代码块中的锁对象可以是任意对象;
  2. 但是必须保证多个线程使用的锁对象是同一个;
  3. 锁对象的作用就是将同步代码块锁住,只允许一个线程在同步代码块中执行;

利用同步代码块实现线程安全的具体代码实现如下所示:

public class RunnableImpl implements Runnable {

    // 定义一个多线程共享的票源
    private int ticket = 100;

    // 创建一个锁对象
    Object object = new Object();

    // 卖票
    @Override
    public void run() {
        while (true) {
            // 创建同步代码块
            synchronized (object) {
                if (ticket > 0) {
                    // 模拟卖票时间
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

同步技术的原理:

同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步,所以就可以保证同步中始终只有一个线程在执行,保证了线程安全。但是程序会频换的判断锁、获取锁和释放锁导致程序的效率降低。

2.2.2、同步方法

使用 synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method(){
	可能会产生线程安全问题的代码
}

利用同步方法实现线程安全的具体代码实现如下所示:

public class RunnableImpl implements Runnable {

    // 定义一个多线程共享的票源
    private int ticket = 100;

    // 卖票
    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    /**
     * 定义一个同步方法
     */
    public synchronized void sellTicket() {
        if (ticket > 0) {

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

            System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

2.2.3、Lock 锁

java.util.concurrent.locks.Lock 机制提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块/同步方法具有的功能 Lock 都有,除此之外更强大,更体现面向对象。

Lock 锁也称同步锁,加锁与释放锁方法化了,如下:

  1. public void lock() : 获取锁;
  2. public void lock() : 释放同步锁;

Lock 锁使用步骤:

  1. 在成员位置创建一个 ReentrantLock 对象;
  2. 在可能出现安全问题的代码前调用 Lock 接口中的方法lock();
  3. 在可能出现安全问题的代码前调用 Lock 接口中的方法unLock();

利用 Lock 锁实现线程安全的具体代码实现如下所示:

public class RunnableImpl implements Runnable {

    // 定义一个多线程共享的票源
    private int ticket = 100;

    // 在成员位置创建一个 ReentrantLock 对象;
    Lock l = new ReentrantLock(); 

    // 卖票
    @Override
    public void run() {
        while (true) {
            l.lock();
            if (ticket > 0) {

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

                System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                ticket--;
            }
            l.unlock();
        }
    }
}

三、小结

多线程的线程安全我们已经讲完了,下一节我们来讲线程状态

系列文章如下:

  1. Java 集合(一)——Collection集合接口、Iterator 迭代器和泛型。
  2. Java 集合(二)——List、Set集合和Collections工具类。
  3. Java 集合(三)——Map 集合。
  4. Java 集合(四)——集合总结。
  5. Java 异常的处理方式与自定义异常。
  6. Java 多线程(一)——多线程实现方式。
  7. Java 多线程(二)——线程安全。

四、源码

文章中用到的所有源码已上传至 github,有需要的可以去下载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值