关于线程安全问题:
1.什么是线程安全问题?.为什么会出现线程安全问题?
首先我们需要了解一个概念,临界资源:即多个线程同时访问的资源(共享资源)。
当多个线程同时操作临界资源的时候,就容易出现线程安全问题,线程安全问题只会影响到线程对同一个共享的全局变量的写操作。
》接下来来演示一下出现线程安全问题的案例:窗口售票
public class Ticket implements Runnable{//创建对象继承Runnable接口 作为参数传入Thread对象构造中
private int ticket=100;//设定临界资源100张票
@Override
public void run() {
while (true){//死循环
if(ticket<=0){票数少于等于0结束循环 不再买票
break;
}
try {
Thread.sleep(10);//休眠只是为了更明显突出多线程安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");//打印卖票
ticket--;//每卖出一张票 票数减一
}
}
}
public class TirketDemo {
public static void main(String[] args) {
Ticket ticket=new Ticket();//创建票对象
new Thread(ticket,"窗口一").start();//作为参数传入
new Thread(ticket,"窗口二").start();
new Thread(ticket,"窗口三").start();
new Thread(ticket,"窗口四").start();
}
}
运行结果:
可以看出来不仅出现重复,还有出现负数票的情况,这是因为在线程运行时候,刚判定结束后,cpu倍另外的线程抢夺而出现的问题,再次醒来之后,票数已经被其他线程修改,该线程仍然继续向下执行减一 就有可能出现负数或者重复。解决方法大致有三种:
一同步代码块
说到同步代码块就要说锁了,一个线程在访问临界资源的时候,如果给这个资源“上一把锁”,这个时候如果其他线程也要访问这个资源,就得在“锁”外面等待。
锁对象可以是任意对象,但是要求必须是同一把锁才行。
加进同步代码块的代码,执行时候其他线程必须等待,待当前进入的线程执行完,释放锁对象之后其他线程才能进去执行,以此类推。
同步中的线程没有执行完毕不会释放锁,同步外的线程没有锁对象,进不去同步
代码实现:加了同步代码块之后就不会出现以上问题
public class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
synchronized (this) {//在这里加上同步代码块并传入所对象
if(ticket<=0){
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");
ticket--;
}
}
}
二.同步方法
创建一个方法,该方法必须用synchronized关键字修饰,将出现安全问题的代码放入到同步方法中,再在Runnable实现类重写的run方法中调用该方法即可。
同步方法默认的锁对象是调用该方法的类 也就是this
public class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true) {
sellTicket();
}
}
public synchronized boolean sellTicket(){
//注意:while循环不能加到方法里面 因为这样的话 调用一次方法 一个线程就会把票卖完了 这里我们返回boolean来判定
if(ticket<=0){//票数少于等于0结束循环 不再买票
return false;
}
try {
Thread.sleep(10);//休眠只是为了更明显突出多线程安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");//打印卖票
ticket--;//每卖出一张票 票数减一
return true;
}
》静态同步方法:即在方法加静态修饰符,静态方法的锁不能是this,因为this只有在类加载之后创建对象才会有,而静态方法在创建对象之前就已经加载,所以同步静态方法的锁是本类的class
public class Ticket implements Runnable{
private static int ticket=100;//静态方法只能访问静态变量
@Override
public void run() {
while (true) {
sellTicket();
}
}
public synchronized static boolean sellTicket(){
//注意:while循环不能加到方法里面 因为这样的话 调用一次方法 一个线程就会把票卖完了
if(ticket<=0){//票数少于等于0结束循环 不再买票
return false;
}
try {
Thread.sleep(10);//休眠只是为了更明显突出多线程安全问题
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");//打印卖票
ticket--;//每卖出一张票 票数减一
return true;
}
三.Lock锁
从jdk1.5之后加入新的接口 Lock,ReentrantLock是Lock接口的实现类。
通过显式定义同步锁对象来实现同步,同步锁提供了比synchronized代码块更广泛的锁定操
注意:最好将 unlock的操作放到finally块中
通过使用ReentrantLock这个类来进行锁的操作,它实现了Lock接口,使用ReentrantLock可以显式地加锁、释放锁。
lock锁使用步骤:1.在成员位置创建一个Reentrantlock对象,因为lock是一个接口需要实现类才能创建对象。
2.在可能会出现安全问题的代码前调用接口方法中的lock()方法获取锁
3.在可能会出现安全问题的代码后面调用接口方法中的Unlock()方法释放锁
注意:最好将有安全问题的代码放到try…finally{ … }中,finally中用于释放锁。
代码:
public class Ticket implements Runnable{
private int ticket=100;
Lock lock=new ReentrantLock();//创建锁对象
@Override
public void run() {
while (true){
lock.lock();//获取锁
try {
if(ticket<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");
ticket--;
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}