假设一个问题:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
分析:
3个窗口售票,互不影响,同时进行。
3个窗口共同出售这100张电影票
public class Demo {
public static void main(String[] args) {
// 创建并启动3个线程
Window window1 = new Window();
Window window2 = new Window();
Window window3 = new Window();
window1.setName("窗口A");
window2.setName("窗口B");
window3.setName("窗口C");
// 启动线程
window1.start();
window2.start();
window3.start();
}
}
class Window extends Thread {
//重写run方法
static int tickets = 100;
@Override
public void run() {
// 循环卖票
while (true) {
// 只有票数>0才允许出售
if (tickets > 0) {
try {
// 这里模拟网络延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+ "卖了第"+ tickets-- + "票");
}
}
}
}
你试试看用IDEA运行,那结果就是一摊屎。
再加入售票延迟后,再次运行我们的仿真程序,此时我们就发现了问题:
相同的票,卖了多次(多卖)
卖出了不存在的票(即超卖)
分析产生多线程数据安全问题的原因:
1.多线程环境
2.访问了共享数据
3.非原子操作(原子操作:一个操作,要么一次执行完,要么不做)
1和2是需求决定的,我们不能做更改
那么如何解决多线程数据安全问题:
先从逻辑上,解决把一组操作变成原子操作这件事情
思路1:如果我们能在一个线程,对共享变量的一组操作的执行过程,能够阻止线程切换,
那么很自然的,这一组操作就变成了原子操作。
但是思路1我们实现不了: 抢占式线程调度,代码层面(线程中执行的代码),无法控制线程调度
思路2:我们无法阻止线程切换,但是我们换个思路,我们给共享变量,加一把锁,利用锁来实现原子操作
使用锁,可以给共享变量加锁,从而保证:
a. 只有加锁的线程,能够访问到共享变量
b. 而且,在加锁线程,没有完成对共享共享变量的,一组操作之前,不会释放锁,
c. 只要不释放锁.其他线程,即使被调度执行,也无法访问共享变量
结果如下
public class Demo2 {
public static void main(String[] args) {
// 创建子类对象
SellWindow sellWindow = new SellWindow();
// 创建3个线程代表3个窗口
Thread t1 = new Thread(sellWindow);
Thread t2 = new Thread(sellWindow);
Thread t3 = new Thread(sellWindow);
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
// 启动线程
t1.start();
//t2.start();
//t3.start();
}
}
class SellWindow implements Runnable {
int tickets = 100;
@Override
public void run() {
// 去卖票
while (true) {
// 判断 票数必须大于0
// 重复的票:假设A窗口抢到CPU的执行权 ,假设此时是第100张票
// 不存在的票:假设A窗口抢到了CPU的执行权,假设是第1张票
if (tickets > 0) {
// 模拟网络延时操作
try {
// A在这睡了0.2s,此时发生了线程切换
// B抢到了CPU的执行权,再发生线程切换 C也可以进来
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"卖出了第" + tickets-- + "票");
// tickets--操作
// 第一步,取值 ,第二步,做-1运算,第三步,重新赋值
// 假设A执行到tickets--的第一步 100
// 此时发生了线程切换 B抢到了Cpu的执行权 100
// 若再次发生线程切换 C抢到了CPU的执行权 100
// 最坏情况
// 输出 A窗口卖出了第1张票,还剩0张
// B窗口 卖出了第0张票 ,还剩-1张
// C窗口 卖出了第-1张票,还剩-2张
}
}
}
}
下面是锁对象的内容(之后还会补充):
synchronized(锁对象) {
需要同步的代码块
}
问题的关键就是锁对象?
锁对象可以是任意对象吗?可以是任意的java对象,但是必须是同一个锁对象。
同步代码块的细节:
a. synchronized代码块中的锁对象,可以是java语言中的任意对象(java语言中的任意一个对象,
都可以充当锁的角色仅限于synchronized代码块中):
1)因为java中所有对象,内部都存在一个标志位,表示加锁和解锁的状态
2)所以其实锁对象,就充当着锁的角色
所谓的加锁解锁,其实就是设置随对象的标志位,来表示加锁解锁的状态。
b. 我们的代码都是在某一条执行路径(某一个线程中运行),当某个线程执行到同步代码块时,
会尝试在当前线程中,对锁对象加锁
1) 此时,如果锁对象处于未加锁状态,jvm就会设置锁对象的标志位(加锁),并在锁对象中记录,
是哪个线程加的锁
然后,让加锁成功的当前线程,执行同步代码块中的代码
2) 此时,如果锁对象已经被加锁,且加锁线程不是当前线程,系统会让当前线程处于阻塞状态(等着),
直到加锁线程,执行完了对共享变量的一组操作,并释放锁
c. 加锁线程何时释放锁?
当加锁线程,执行完了同步代码块中的代码(对共享变量的一组操作),在退出同步代码块之前,
jvm自动清理锁对象的标志位,将锁对象变成未上锁状态(释放锁)
千万要注意:
a. 虽然,synchronized代码块,中的锁对象,可以是java语言中的任意对象
b. 但是,在多线程运行环境下,想要让访问 同一个共享变量的, 多个synchronized代码块中的代码是原子操作
注意,对同一个共享变量的访问,必须使用同一个锁对象。
public class Demo {
public static void main(String[] args) {
// 创建3个线程并启动
SellWindow sellWindow = new SellWindow();
Thread t1 = new Thread(sellWindow);
Thread t2 = new Thread(sellWindow);
Thread t3 = new Thread(sellWindow);
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
t1.start();
t2.start();
t3.start();
}
}
class SellWindow implements Runnable {
int tickets =100;
//A objA = new A();
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+"卖了第" + tickets-- +"票");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class A {
}
同步方法中的锁对象就是this
静态方法中的锁对象是字节码文件对象 .class Class
public class Demo2 {
public static void main(String[] args) {
SellWindow2 sellWindow2 = new SellWindow2();
Thread t1 = new Thread(sellWindow2);
Thread t2 = new Thread(sellWindow2);
Thread t3 = new Thread(sellWindow2);
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
//start
t1.start();
t2.start();
t3.start();
}
}
class SellWindow2 implements Runnable {
static int tickets = 100;
//Object obj = new Object();
B b = new B();
int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
synchronized (SellWindow2.class) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖了第" + tickets-- + "票");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} else {
sell();
}
i++;
}
}
static private synchronized void sell() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖了第" + tickets-- + "票");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class B {
}
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
Lock锁对象 VS synchoronized 锁对象
区别:1. synchronized 锁对象,只提供了用来模拟锁状态的标志位(加锁和释放锁),但是加锁和释放锁的行为,都是由jvm隐式完成(和synchronized 锁对象没关系),所以synchronized 锁对象不是一把完整的锁;
2.一个Lock对象,就代表一把锁,而且还是一把完整的锁, Lock对象,它如果要实现加锁和释放锁,不需要synchronized关键字配合,它自己就可以完成
Lock(接口): lock() 加锁;unlock 释放锁。
3. 两种锁对象,实现完全不同的 联系: 都可以实现线程同步
1. synchronized(锁对象) {需要同步的代码}
2. lock.lock()需要同步的代码lock.unlock()
学习了Lock锁对象之后,我们就有两种方式,构造同步代码块,从而实现线程通过(构造原子操作),实际开发的时,使用哪种方式呢? 推荐 synchronized代码块
1. 两种方式,实现线程同步,效果相同,但是 使用synchronized代码块的方式要简单的多; 2. 虽然说在jdk早期版本中,两种方式加锁和释放锁,确实有效率上的差别,Lock锁机制加锁释放锁效率高一些,但是,在今天的jdk中,两种方式加锁和释放锁的效率已经相差无几了。
public class Demo3 {
public static void main(String[] args) {
SellWindow3 sellWindow3 = new SellWindow3();
Thread t1 = new Thread(sellWindow3);
Thread t2 = new Thread(sellWindow3);
Thread t3 = new Thread(sellWindow3);
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
t1.start();
t2.start();
t3.start();
}
}
class SellWindow3 implements Runnable {
Lock lock = new ReentrantLock();
int tickets = 100;
@Override
public void run() {
// 卖票
while (true) {
// lock() 加锁
lock.lock();
try {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+"卖了第" + tickets-- + "票");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
// unlock()释放锁
lock.unlock();
}
}
}
}
这一篇可能讲的不全,我会继续补充。
可以参考陈旭远:线程安全问题:卖票案例实现zhuanlan.zhihu.com