线程安全出现的原因
这里我们让多个线程共享同一个卖票资源:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL0EFRNdXRE5keRpHW3BjMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL5YjM5IDOwkTMzATNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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的使用权。等到它睡醒之后,它照样还是会往下执行, 不会管其它票的数量已经减少了。
线程安全的解决办法
1. 线程同步(synchronized)
同步代码块
-
关键字可以用于方法中的某个区块中,表示对这个区块的互斥访问同步代码块synchronized
synchronized(同步锁){
需要同步互斥操作的代码
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
-
锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
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--;
}
}
}
}
}
线程类中创建的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()
使用步骤:
- 在成员位置创建一个
ReentrantLock对象
- 在可能会出现安全问题的代码前调用
中的方法Lock接口
lock获取
- 在可能会出现安全问题的代码后调用
中的方法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();
}
}
}
}
}