java 线程锁 对象_从0开始的Java:线程安全与锁对象

假设一个问题:

假设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.com722d3831bff5558df924e100b9524547.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值