JavaEE | 线程安全(锁、线程间通信、内存可见性、CAS、线程的状态)
1.案例与线程安全
1.1 两种方式模拟电影院售票
案例:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
通过两种方式实现:
1.通过Runnable接口实现
2.通过Thread类实现
1.通过Runnable接口实现
public class MyTest1 {
public static void main(String[] args) {
//某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
//通过实现Runnable接口实现
MyRunnable myRunnable = new MyRunnable();
Thread th1 = new Thread(myRunnable);
Thread th2 = new Thread(myRunnable);
Thread th3 = new Thread(myRunnable);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
public class MyRunnable implements Runnable{
private static int num=100;
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true){
if(num>=1){
try {
// 售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟
// 改实现接口方式的卖票程序,每次卖票延迟100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"售出第"+(num--)+"张票");
}else
{
break;
}
}
}
}
2.通过Thread类实现
public class MyTest1 {
public static void main(String[] args) {
//某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
//通过继承Thread类实现
MyThread th1 = new MyThread();
MyThread th2 = new MyThread();
MyThread th3 = new MyThread();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
public class MyThread extends Thread {
//此处一定要写static,用来共享数据
private static int num=100;
@Override
public void run() {
String name = this.getName();
while(true){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(num>1){
System.out.println(name+"售出第"+(num--)+"张票");
}
else{
break;
}
}
}
}
注意:如果使用Thread类的继承类实现,共享变量(余票数量)一定要设置为静态变量
显然,本案例中,用Runnable接口实现的方式更好用。
1.2 买电影票出现了同票和负数票的原因分析
出现相同票的原因:是由于原子性所导致的。 "num–"不是一个原子性的操作,所谓原子性,指的是不可再分割。num–实际上有两步执行的,先运算,再自减。在num参与运算的过程中,如果有其他线程使用num会导致出现相同票。
出现0票或者负数票的原因:实际原因和出现相同票的原因相同,也是由于num–的非原子性所导致的。只是出现该问题的时间不同,当num=1时,多个线程同时进入循环,会导致粗线0或负值。
2. 保证线程安全之synchronized
2.1 概述
何种情况下会出现线程安全问题:
- 多线程环境
- 有共享数据
- 多条语句操作共享数据
分析:现在的这个售票程序是存在问题的,因为它满足上面的标准,那么我们只要将这个标准打乱,那么我们就可以解决这个问题.
而上面的标准中1 , 2是不能打乱的,因此我们只能对c做处理,关键是怎么处理? 如果我们把操作共享数据的多条语句看做成一个整体,当一个线程执行这个整体的时候,其他的线程处于等待状态,也就说当一个线程执行这个整体的时候,其他线程不能进行执行,那么怎么做到这个一点呢?
下面引入同步代码块,同步方法和同步静态方法。
2.2 同步代码块
格式:synchronized(对象){ //不能在括号了直接new 对象 new 了 就没效果
要被同步的代码 ;
}
- 这个同步代码块保证数据的安全性的一个主要因素就是这个对象被所有的线程对象所共享
- 这个对象要定义为静态成员变量,才能被所有线程共享
- 这个对象其实就是一把锁.
- 这个对象习惯叫做监视器
同步代码块的锁对象: 任意一个对象
public class MyTest1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread th1 = new Thread(myRunnable);
Thread th2 = new Thread(myRunnable);
Thread th3 = new Thread(myRunnable);
th1.setName("窗口一");
th2.setName("窗口二");
th3.setName("窗口三");
th1.start();
th2.start();
th3.start();
}
}
public class MyRunnable implements Runnable {
static int num=100;
static Object obj=new Object();
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true)
{
synchronized (obj){
//锁 ,其实就是一个任意对象,多个线程要共享一把锁
if(num>=1){
try {
Th