1、概述
当多个线程运行同一个任务时,很容易出现线程安全问题。
2、举例:卖票问题,以此来揭示线程安全问题。代码如下:
public class Thread01SafeTest01 extends Thread{
public static void main(String[] args) {
Runnable runnable = new Ticket(); //创建一个卖票的任务叫runnable
Thread thread1 = new Thread(runnable); //创建第一个线程,命名卖票线程一,并启动
thread1.setName("卖票线程一");
thread1.start();
Thread thread2 = new Thread(runnable); //创建第二个线程,命名卖票线程二,并启动
thread2.setName("卖票线程二");
thread2.start();
Thread thread3 = new Thread(runnable); //创建第三个线程,命名卖票线程三,并启动
thread3.setName("卖票线程三");
thread3.start();
}
//类:卖票
static class Ticket implements Runnable{
//票数计数器count
private int count =10;
@Override
public void run() {
while(count>0){
System.out.println("正在准备买票"); //这里是卖票操作,每卖一张,就输出一次。
/*这里的try--catch结构,进行停留一秒,是为了留出一定的时间,以便在运行结果中看得出由于线程导致的运行结果错误。
假设现在count=1,,三个线程经过了判断,都处在 while(count>0)和 count--; 这两句之间运行时,当其中一个进行了
count--,紧接着第二个,第三个会依次进行 count--;就会出现输出count=-1,count=-2这样的情况,而按我们的现实
中的情况而言,很明显,当票没有了,怎么可能还有票卖呢,最后面买票的人岂不是没有座位可以坐,因为票卖超了。
所以解决办法是:当有线程在卖票的时候,另外一个线程必须要等着前面的线程卖完了一张,才能接着进入卖票的代码块,
我们可以通过Synchronized来控制这个过程。*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //卖票操作完成后,票数减一
//告知哪个线程卖的票,和余票数量
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
}
}
}
}
运行结果:
3个线程共同卖票,结果票卖超了。必须对线程加以控制,所存在的问题在代码中已有注释。
3、解决办法
3.1同步代码块(代码和上述代码基本一样,主要看“=”行标示)
public class Thread02SafeTest02Synchronized extends Thread {
/**
* 主要主题:线程同步:synchronized
* 一、线程不安全
* 解决方案一:同步代码块(即需要排队执行的代码块)
* 格式:synchronized(被锁的对象){
* 同步代码块
* }
* @param args
*/
//下面为主程序main
public static void main(String[] args) {
Runnable runnable2= new Ticket(); //创建一个卖票的任务叫runnable
Thread thread1 = new Thread(runnable2); //创建第一个线程,命名卖票线程一,并启动
thread1.setName("卖票线程一");
Thread thread2 = new Thread(runnable2); //创建第二个线程,命名卖票线程二,并启动
thread2.setName("卖票线程二");
Thread thread3 = new Thread(runnable2); //创建第三个线程,命名卖票线程三,并启动
thread3.setName("卖票线程三");
thread1.start();
thread2.start();
thread3.start();
}
//类:卖票
static class Ticket implements Runnable{
//票数计数器count
private int count =20;
//用来被锁的对象,需要创建run()方法之前,因为作为被锁的对象只能是一个,才能实现多线程排队执行的效果。
private Object object = new Object();
//重写run方法
@Override
public void run() {
// private Object object = new Object();如果在这里创建,那么每个线程运行时都创建了一把锁,那么就实现不了锁的控制作用了。
while (true) {
//synchronized把下面两行“=”之间的代码块,关起来了,并且还把门上了一把锁,这把锁就是下面
// synchronized(object)中的object,所以多个线程此时只能向上厕所一样,只有前面的线程执行完毕,
// 锁开了才能进去,即排队一个一个进去执行,就避免了类ThreadSafeTest01运行结果中的线程安全问题。
synchronized(object) {
//同步代码块
//========================================================================================
if (count > 0) {
System.out.println("正在准备买票"); //这里是卖票操作,每卖一张,就输出一次。
try {
Thread.sleep(100); //停顿一下,方便看输出结果。
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //卖票操作完成后,票数减一
//告知哪个线程卖的票,和余票数量
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
}else{
break;
}
//========================================================================================
}
}
}
}
}
运行结果:
3.2 同步方法(代码和上述代码基本一样,主要看“=”行标示)
public class Thread03SafeTest03Synchronized {
/**
* 主要主题:线程同步:synchronized
* 一、线程不安全
* 解决方案二:同步方法(把需要排队执行的代码块,或者说多线程执行时会造成混乱的代码,单独提出来做成方法即可)。
* 格式:synchronized 方法名(){
* 代码块
* }
* 即:用synchronized修饰方法。
* 注意:
* 1、在同步方法的做法下,哪个对象调用了synchronized修饰的方法,那这个对象就是那把控制多线程排队执行的锁,即synchronized锁的
* 是当前对象,也就是this。
* 2、如果同时存在同步代码块和同步方法,且锁相同,比如都是this,相当于两个试衣间只建了一条门,用的是同一把锁。
* @param args
*/
public static void main(String[] args) {
Runnable runnable3 = new Ticket(); //创建一个卖票的任务叫runnable
Thread thread1 = new Thread(runnable3); //创建第一个线程,命名卖票线程一,并启动
thread1.setName("卖票线程一");
Thread thread2 = new Thread(runnable3); //创建第二个线程,命名卖票线程二,并启动
thread2.setName("卖票线程二");
Thread thread3 = new Thread(runnable3); //创建第三个线程,命名卖票线程三,并启动
thread3.setName("卖票线程三");
thread1.start();
thread2.start();
thread3.start();
}
//类:卖票
static class Ticket implements Runnable{
//票数计数器count
private int count =20;
//重写run方法
@Override
public void run() {
while (true) {
boolean flag = sale(); //这里调用了方法sale();
if (!flag){
break;
}
}
}
//给方法添加synchronized
//============================================================================================================
public synchronized boolean sale(){
if (count > 0) {
System.out.println("正在准备买票"); //这里是卖票操作,每卖一张,就输出一次。
try {
Thread.sleep(1000); //停顿一下,方便看输出结果。
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //卖票操作完成后,票数减一
//告知哪个线程卖的票,和余票数量
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
return true;
}else{
return false;
}
}
//============================================================================================================
}
}
运行结果:
3.3 显示锁Lock(代码和上述代码基本一样,主要看“=”行标示)
public class Thread04SafeTest04Lock {
/**
* 主要主题:线程同步:Lock
* 一、线程不安全
* 解决方案三:显示锁(即把需要排队执行的代码块锁起来)
* 显示锁:Lock 子类 ReentrantLock
* @param args
*/
//下面是主程序
public static void main(String[] args) {
Runnable runnable3 = new Ticket(); //创建一个卖票的任务叫runnable
Thread thread1 = new Thread(runnable3); //创建第一个线程,命名卖票线程一,并启动
thread1.setName("卖票线程一");
Thread thread2 = new Thread(runnable3); //创建第二个线程,命名卖票线程二,并启动
thread2.setName("卖票线程二");
Thread thread3 = new Thread(runnable3); //创建第三个线程,命名卖票线程三,并启动
thread3.setName("卖票线程三");
thread1.start();
thread2.start();
thread3.start();
}
//类:卖票
static class Ticket implements Runnable {
//票数计数器count
private int count = 20;
//========================================================================================
//创建一个锁
Lock l = new ReentrantLock();
//Lock l = new ReentrantLock(true); //如果fair参数为true,那么显示锁就为公平锁
//========================================================================================
//重写run方法
@Override
public void run() {
while (true) {
//========================================================================================
l.lock(); //在此处上锁
//====================================下方为需要锁起来的代码块===============================
if (count > 0) {
System.out.println("正在准备买票"); //这里是卖票操作,每卖一张,就输出一次。
try {
Thread.sleep(100); //停顿一下,方便看输出结果。
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //卖票操作完成后,票数减一
//告知哪个线程卖的票,和余票数量
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
} else {
break;
}
//========================================================================================
l.unlock(); //在此处解锁
//========================================================================================
}
}
}
}
运行结果:
4、总得来说,线程安全基本解决办法为上述三种,其本质对容易产生线程安全问题的代码块进行控制。当然还有公平锁、以及线程锁死等问题。