线程安全问题概述
卖票问题分析
- 单窗口卖票
一个窗口(单线程)卖100张票没有问题
单线程程序是不会出现线程安全问题的
- 多个窗口卖不同的票
3个窗口一起卖票,卖的票不同,也不会出现问题
多线程程序,没有访问共享数据,不会产生问题
- 多个窗口卖相同的票
3个窗口卖的票是一样的,就会出现安全问题
多线程访问了共享的数据,会产生线程安全问题
线程安全问题代码实现
模拟卖票案例
创建3个线程,同时开启,对共享的票进行出售
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
线程安全问题原理分析
线程安全问题产生原理图
分析:线程安全问题正常是不允许产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权;让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票。
解决线程安全问题办法1-synchronized同步代码块
同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
使用synchronized同步代码块格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
代码实现如下:
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//创建一个锁对象
Object obj = new Object();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
//同步代码块
synchronized (obj){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
⚠️注意:
- 代码块中的锁对象,可以使用任意的对象。
- 但是必须保证多个线程使用的锁对象是同一个。
- 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
同步技术原理分析
同步技术原理:
使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器
3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。
- t0抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t0会检查synchronized代码块是否有锁对象
发现有,就会获取到锁对象,进入到同步中执行
- t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t1会检查synchronized代码块是否有锁对象
发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象,t0线程执行完同步中的代码,会把锁对象归 还给同步代码块t1才能获取到锁对象进入到同步中执行
📌总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
解决线程安全问题办法2-synchronized普通同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void payTicket(){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
代码实现:
public /**synchronized*/ void payTicket(){
synchronized (this){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
分析:
定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。
🧐同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl(),也是就是this,所以同步方法是锁定的this对象。
解决线程安全问题办法3-synchronized静态同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
格式:
public static synchronized void payTicket(){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
代码实现:
public static /**synchronized*/ void payTicketStatic(){
synchronized (RunnableImpl.class){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket--;
}
}
}
分析:🧐静态的同步方法锁对象是谁?
不能是this,this是创建对象之后产生的,静态方法优先于对象
静态方法的锁对象是本类的class属性-->class文件对象(反射)。
解决线程安全问题办法4-Lock锁
Lock接口中的方法:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁
使用步骤:
- 在成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
代码实现:
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//1.在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
//2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
l.lock();
try {
//先判断票是否存在
if(ticket>0) {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
l.unlock();
//3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
//无论程序是否异常,都会把锁释放
}
}
}
分析:java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
公平锁、非公平锁的创建方式:
//创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
//创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);
- 锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
ReentrantLock和Synchronized的区别
相同点:
- 它们都是加锁方式同步;
- 都是重入锁;
- 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);
不同点 | SynChronized | ReentrantLock(实现了 Lock接口) |
---|---|---|
原始构成 | 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 | 它是JDK 1.5之后提供的API层面的互斥锁类 |
实现 | 通过JVM加锁解锁 | api层面的加锁解锁,需要手动释放锁。 |
代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成, |
灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock因为是方法调用,可以跨方法,灵活性更大 |
等待可中断 | 不可中断,除非抛出异常(释放锁方式:1.代码执行完,正常释放锁;2.抛出异常,由JVM退出等待) | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行) |
是否公平锁 | 非公平锁 | 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁, |
条件Condition | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能. | |
提供的高级功能 | 提供很多方法用来监听当前锁的信息,如:getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked() | |
便利性 | Synchronized的使用比较方便简洁,由编译器去保证锁的加锁和释放 | 需要手工声明来加锁和释放锁, |
适用情况 | 资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好 | ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。 |