线程的安全问题
第四节 线程的安全问题
文章目录
前言
相信大家在学习java基础的过程中,总会听到一些方法涉及线程的安全与否的问题,到底什么是线程的安全呢?接下来将会针对这个问题来进行解答。
一、线程安全是什么?
假设你在工行有一个银行账户,两张银联卡(自己手里一张,女朋友手里一张),里面有100万。假设取钱就两个过程:1.检查账户余额,2.取出现金(如果要取出的金额 > 账户余额,则取现成功,否则取现失败)。有一天你要买房想把钱取出来,而此时你女朋友也想买一辆车(假设你们事先没有商量)。两个人都在取钱,你在A号ATM机取100万,女朋友在B号ATM机取80万。这时A号ATM检查账户余额发现有100万,可以取出;而与此同时,同一时刻B号ATM也在检查账户余额发现有100万,可以取出;这样,A、B都把钱取出来了。
100万的存款取出180万,银行就出现问题了。这就是线程并发的不安全性。为避免这种情况发生,我们要将多个线程对同一数据的访问同步,确保线程安全。
所谓同步(synchronization)就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。同步最常见的方式就是使用锁(Lock),也称为线程锁。锁是一种非强制机制,每一个线程在访问数据或资源之前,首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁被占用时试图获取锁,线程会进入等待状态,直到锁被释放再次变为可用。这样就可以保证线程对共享数据从之前的并行操作改为串行操作,这样就保证了数据的安全性了。
在极端情况下,即你和女朋友同时取钱,银行认为你们的操作都是没有问题的时候,银行将会出现错误的处理结果。
- 问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。 - 解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
二、线程安全的实现方式
需求:实现一个售票的功能,要求3个窗口同时对100张票进行售卖,不得出现错票。
分析:
- 三个窗口,即三个线程,所以这是一个多线程程序。
- 一共有100张票,即这三个线程共享这100张票,所以存在线程安全问题,需要进行安全处理。
一、Synchronized关键字
1.同步代码块实现
- 格式:
synchronized (同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。){
// 需要被同步的代码;
}
- 实现需求代码
class ThreadTest1 implements Runnable{
//定义共享属性,票
private int ticket = 100;
//定义同步监视器
//同步监视器对象的选用很关键。要选择线程共享的对象,比如下面例子的 threadTest1, 它是static修饰的才行,
// 如果没有static修饰,则是使用不同的同步监视器(不是同一个对象),相当于是两把钥匙。
static ThreadTest1 threadTest1 = new ThreadTest1();
@Override
public void run() {
while (true){
//定义同步代码块
synchronized (threadTest1){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
//测试
public class ThreadSynTest1{
public static void main(String[] args) {
ThreadTest1 threadTest1 = new ThreadTest1();
Thread t1 = new Thread(threadTest1);
Thread t2 = new Thread(threadTest1);
Thread t3 = new Thread(threadTest1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.同步方法实现
synchronized还可以放在方法声明中,表示整个方法为同步方法。
- 格式:
public synchronized void xxx(String name){
// 需要被同步的代码;
}
- 实现需求代码
class ThreadTest2 implements Runnable{
private int ticket = 100;
@Override
public void run() {
//此处使得程序一直在运行,运行结束后请手动关闭
while (true){
//调用同步方法
show();
}
}
//实现同步方法
private synchronized void show(){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class ThreadSynTest2 {
public static void main(String[] args) {
ThreadTest2 threadTest2 = new ThreadTest2();
Thread t1 = new Thread(threadTest2);
Thread t2 = new Thread(threadTest2);
Thread t3 = new Thread(threadTest2);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
二、使用Lock(锁) JDK5.0新增
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
- 格式:
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
- 实现需求代码
class ThreadTest3 implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class ThreadLockTest3 {
public static void main(String[] args) {
ThreadTest3 threadTest3 = new ThreadTest3();
Thread t1 = new Thread(threadTest3);
Thread t2 = new Thread(threadTest3);
Thread t3 = new Thread(threadTest3);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
三、线程安全的相关补充
一、释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
二、不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程。
三、线程的死锁问题
当不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行。
四、synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)