一、线程安全问题
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()
:释放同步锁
使用须知:
1、java.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来看看