Java多线程Part2:线程(Thread)的安全问题和同步机制

一、线程安全问题

1、概述

单线程程序不会出现安全问题
就像电影院只有一个售票窗口 这个窗口售卖所有的票

多线程程序 没有访问共享数据 也不会产生问题
就像电影院有三个售票窗口 每个窗口售卖三分之一不重复的票

多线程程序 访问共享数据 此时会产生问题
就像电影院有三个售票窗口 每个窗口售卖所有的票
★比如说 第一个窗口卖第100号票 第二个窗口也在卖第100号票
此时出现了重复的票
★比如说 第一个窗口卖第1号票 第二个窗口在卖第0号票 第三个窗口在卖第-1号票
此时出现了不存在的票
三个窗口卖的票都是一样的 就会出现安全问题
当多线程访问了共享的数据 会产生线程安全问题

2、产生原理

我们拿卖票作为例子:

// 多个线程共享的票源
private int ticket=1;

@Override
public void run() {
    // 使用死循环 让卖票操作重复执行
    while (true)
    {
        // 先判断票是否存在
        if (ticket>0)
        {
        	// 让线程休眠10毫秒
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

出现重复的票:

假设有3个线程 线程t0 t1 t2同时执行到了打印语句的这句话
此时 剩余票数都是一致的 因为还没执行到让票数-1的代码
因而此时会出现重复的票

出现不存在的票:

假设共有1张票
假设开启了3个线程 t0 t1 t2
3个线程一起抢夺CPU的执行权 谁抢到谁执行

当t0抢到CPU的执行权 进入run方法中执行
睡眠(sleep)后 即放弃了CPU的执行权

然后t2抢到CPU的执行权 进入run方法中执行
睡眠(sleep)后 放弃了CPU的执行权

最后t1抢到CPU的执行权 进入run方法中执行
睡眠(sleep)后 放弃了CPU的执行权

t2先睡醒了 抢到了CPU的执行权 继续执行
进行卖票工作(打印语句) ===> 1
票数减1 继续判断 剩余票数不大于0 不执行

t1睡醒了 抢到了CPU的执行权 继续执行
进行卖票工作 此时票已经为0 打印语句 ===> 0
票数减1 此时票已经为-1
继续判断 剩余票数不大于0 不执行

t0睡醒了 抢到了CPU的执行权 继续执行
进行卖票工作 此时票已经为-1 打印语句 ===> - 1
票数减1 此时票已经为-2
继续判断 剩余票数不大于0 不执行

如此 导致出现不存在的票

线程安全问题是不能产生的
可以让一个线程再访问共享数据的时候 无论是否失去了CPU的执行权 让其它的线程只能等待
等待当前线程的代码全部执行完业务代码 其它线程再执行
确保始终只有一个线程在执行业务代码

二、解决方法:同步技术

有三种方式可以解决 分别是:
1、同步代码块
2、同步方法
3、锁(Lock)机制

让我们逐个学习:

1、同步代码块

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

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

1、代码块中的锁对象可以使用任意对象
2、但必须保证多个线程使用的锁对象是同一个
锁对象的作用:将同步代码块锁住 只让一个线程在同步代码块中执行

public class MyThreadSynchronized implements Runnable{
    // 定义一个多个线程共享的票源
    private int ticket=100;

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

    // 线程任务:卖票
    @Override
    public void run() {
        // 使用死循环 让卖票操作重复执行
        while (true)
        {
            // 同步代码块
            synchronized (object)
            {
                // 先判断票是否存在
                if (ticket>0)
                {
                	// 让线程休眠10毫秒
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

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

同步技术的原理

使用了一个锁对象 这个锁对象叫作同步锁 也叫对象监视器
假若有3个线程 3个线程一起抢夺CPU的执行权 谁抢到了就执行谁的run()方法

当t0线程抢到了CPU的执行权 执行它的run()方法 会遇到synchronized同步代码块
这时t0会检查synchronized代码块是否有锁对象 然后发现有
这时 它会获取到这个锁对象 带着它进入 执行里面的方法

然后t1抢到了CPU的执行权 执行它的run()方法 遇到synchronized同步代码块
这时t1会检查synchronized代码块是否有锁对象 然后震惊地发现竟然没有(2333
t1就会进入阻塞状态 一直等待t0线程归还对象
一直到t0线程执行完同步中的代码 随后将锁对象归还给同步代码块
此时 t1才能获取到锁对象 然后带着这个锁对象进入 执行里面的方法

总结:同步中的线程没有执行完毕是不会释放锁的 然后同步外的线程没有锁就进不去 只能在外面等(就像上厕所里面有人了
同步保证了只能有一个线程在同步中使用共享数据 确保了安全 这是好的方面
然而 程序频繁判断锁 获取锁 释放锁 程序的效率会有所降低


2、同步方法

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

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

普通同步方法

public class MyThreadSynchronized implements Runnable{
    // 多个线程共享的票源
    private int ticket=100;

    // 定义一个同步方法
    public synchronized void payTicket()
    {
        // 先判断票是否存在
        if (ticket>0)
        {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

    // 线程任务:卖票
    @Override
    public void run() {
        // 使用死循环 让卖票操作重复执行
        while (true)
        {
            payTicket();
        }
    }
}

除了这种普通的同步方法以外 还有一种静态同步方法 顾名思义 方法是静态的(废话

静态同步方法

public class MyThreadSynchronized implements Runnable{
    // 多个线程共享的票源
    private static int ticket=100;

    // 定义一个静态同步方法
    public static synchronized void payTicketStatic()
    {
        // 先判断票是否存在
        if (ticket>0)
        {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

    // 线程任务:卖票
    @Override
    public void run() {
        // 使用死循环 让卖票操作重复执行
        while (true)
        {
            payTicketStatic();
        }
    }
}

我们会发现 在同步方法中 我们没有传进去锁对象
那么此时 锁对象是谁?
1、对于非static方法 同步锁就是this
2、对于static方法 不能再是this了 因为this是创建对象之后所产生的 而静态方法优先于对象
因此使用的是当前方法所在类的字节码对象(即 类名.class)


3、锁(Lock)机制

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

Lock锁也称同步锁 将加锁与释放锁方法化
基本方法如下:
public void lock() :加同步锁
public void unlock() :释放同步锁

使用须知:
1java.util.concurrent.locks.ReentrantLock类实现了Lock接口
因此需在成员位置创建一个ReentrantLock对象
2、在可能会出现安全问题的代码前调用Lock接口中的lock()方法来获取锁
3、在可能会出现安全问题的代码后调用Lock接口中的unlock()方法来释放锁

public class MyThreadSynchronized implements Runnable{
    // 定义一个多个线程共享的票源
    private int ticket=100;

    // 创建一个ReentrantLock锁对象(用到了多态的思想 向上造型)
    Lock lock=new ReentrantLock();

    // 线程任务:卖票
    @Override
    public void run() {
        // 使用死循环 让卖票操作重复执行
        while (true)
        {
            // 获取锁
            lock.lock();
            // 先判断票是否存在
            if (ticket>0)
            {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
                ticket--;
            }
            // 释放锁
            lock.unlock();
        }
    }
}

最后 我们还可以整合一下 让释放锁的行为在finally中完成
无论程序是否出现异常 都要释放掉锁对象

@Override
public void run() {
    // 使用死循环 让卖票操作重复执行
    while (true)
    {
        // 获取锁
        lock.lock();
        // 先判断票是否存在
        if (ticket>0)
        {
            try {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName()+"正在出售第"+ticket+"张票");
                ticket--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 释放锁
                lock.unlock();
            }
        }
    }
}

笔者个人感觉Lock锁方式是最简单的 调用两个方法就完事了
当然 ReentrantLock类中还有其它方法 感兴趣的兄弟可以找API来看看


发布了56 篇原创文章 · 获赞 0 · 访问量 1184

猜你喜欢

转载自blog.csdn.net/Piconjo/article/details/104616864