天天看点

Java中的线程安全出现的原因和解决办法(多个线程共享资源,临界区)

线程安全出现的原因

这里我们让多个线程共享同一个卖票资源:

Java中的线程安全出现的原因和解决办法(多个线程共享资源,临界区)
public class RunnableImpl implements Runnable {
    //总的票得数量
    int tickets = 20;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                //为系统性能考虑,每个线程稍微休息一下
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "的第" + tickets + "张票");
                tickets--;
            }
        }
    }
}
           
public class DemoThreadSafe {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建多个Thread类对象
        Thread r0 = new Thread(run);
        Thread r1 = new Thread(run);
        Thread r2 = new Thread(run);
        //用start方法开启线程
        r0.start();
        r1.start();
        r2.start();
    }
}
           

输出:

Thread-1的第20张票
Thread-2的第20张票
Thread-0的第20张票
Thread-2的第17张票
Thread-0的第16张票
Thread-1的第16张票
Thread-2的第14张票
Thread-0的第13张票
Thread-1的第12张票
Thread-2的第11张票
Thread-0的第11张票
Thread-1的第11张票
Thread-0的第8张票
Thread-2的第8张票
Thread-1的第6张票
Thread-2的第5张票
Thread-0的第4张票
Thread-1的第3张票
Thread-2的第2张票
Thread-0的第2张票
Thread-1的第0张票
Thread-2的第-1张票
           

从上面的例子可以看到, 有被重复卖出去的票,还有第-1,0张票,出现了线程安全问题。因为线程卖出了重复的票和不存在的票。

出现这个问题的原因是,我们创建的每个线程执行到

sleep()

语句之后就放弃了cpu的使用权。等到它睡醒之后,它照样还是会往下执行, 不会管其它票的数量已经减少了。

Java中的线程安全出现的原因和解决办法(多个线程共享资源,临界区)

线程安全的解决办法

1. 线程同步(synchronized)

同步代码块

  • 同步代码块synchronized

    关键字可以用于方法中的某个区块中,表示对这个区块的互斥访问
synchronized(同步锁){
	需要同步互斥操作的代码
}
           

注意:

  1. 通过代码块中的锁对象,可以使用任意的对象
  2. 但是必须保证多个线程使用的锁对象是同一个
  3. 锁对象作用:

    把同步代码块锁住,只让一个线程在同步代码块中执行

public class RunnableImpl implements Runnable {
    //总的票得数量
    int tickets = 20;

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

    @Override
    public void run() {
        while (true) {
            //同步代码块
           synchronized (obj){
               if (tickets > 0) {
                   //为系统性能考虑,每个线程稍微休息一下
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "的第" + tickets + "张票");
                   tickets--;
               }
           }
        }
    }
}
           
Java中的线程安全出现的原因和解决办法(多个线程共享资源,临界区)

线程类中创建的obj锁对象相当于操作系统中的pv操作,每个线程运行到synchronized的时候判断锁是否还有,有就拿来执行,没有就等待别的线程归还锁。

2. 同步方法

  • 使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其它线程只能在方法外等候
public synchronized void method(){
	可能会产生线程安全问题的代码
}
           

同步锁是谁?

对于非static方法,同步锁就是this

对于static方法,是我们使用当前方法所在类的字节码对象(类名.class)

public class RunnableImpl implements Runnable {
    //总的票得数量
    int tickets = 20;

    @Override
    public void run() {
        while (true) {
            //同步代码块
           sellTickets();
        }
    }

    public synchronized void sellTickets(){
        if (tickets > 0) {
            //为系统性能考虑,每个线程稍微休息一下
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "的第" + tickets + "张票");
            tickets--;
        }
    }
}
           

Lock锁

Java util.concurrent.locks.lock

机制提供了比

Synchronized代码块

synchronized方法

更加广泛的锁定操作,同步代码块、同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称作同步锁,加锁和释放锁方法化了:

  • public void lock()

    :加同步锁
  • public void unlock()

    :释放同步锁
使用步骤:
  1. 在成员位置创建一个

    ReentrantLock对象

  2. 在可能会出现安全问题的代码前调用

    Lock接口

    中的方法

    lock获取

  3. 在可能会出现安全问题的代码后调用

    Lock接口

    中的方法

    unlock释放锁

public class RunnableImpl implements Runnable {
    //总的票得数量
    int tickets = 20;

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

    @Override
    public void run() {
        while (true) {
            //2. 在可能会出现安全问题的代码前调用`Lock接口`中的方法`lock获取`
            l.lock();
            if (tickets > 0) {
                //为系统性能考虑,每个线程稍微休息一下
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "的第" + tickets + "张票");
                tickets--;
            }
            //3. 在可能会出现安全问题的代码后调用`Lock接口`中的方法`unlock释放锁`
            l.unlock();
        }
    }
}
           

为了确保锁必须被释放,推荐采用以下的写法,将unlock()放在finally块代码语句中,无论是否出现异常,锁都会被释放。

public class RunnableImpl implements Runnable {
    //总的票得数量
    int tickets = 20;

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

    @Override
    public void run() {
        while (true) {
            //2. 在可能会出现安全问题的代码前调用`Lock接口`中的方法`lock获取`
            l.lock();
            if (tickets > 0) {
                //为系统性能考虑,每个线程稍微休息一下
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "的第" + tickets + "张票");
                    tickets--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3. 在可能会出现安全问题的代码后调用`Lock接口`中的方法`unlock释放锁`
                    l.unlock();
                }
            }
        }
    }
}