一、买票案例
1.1线程不安全方式
package com.learning.concurrent;
/**
* ClassName:TicketWindow
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:33
* @author:
*/
public class TicketWindow {
private int tickets;
public TicketWindow(int tickets) {
this.tickets = tickets;
}
public void sell (int sells) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets - sells >=0) {
tickets = tickets -sells;
}
}
public int getTickets() {
return tickets;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* ClassName:SellTicketTest
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:38
* @author:17611219021@sina.cn
*/
@Slf4j
public class SellTicketTest {
public static void main(String[] args) throws Exception{
//模拟多人买票窗口
TicketWindow window=new TicketWindow(60000);
//线程集合
List<Thread> threadList = new ArrayList<Thread>();
//统计一共卖出了多少张票
Vector<Integer> sellCounts = new Vector<>();
long start = System.currentTimeMillis();
log.info("开始时间:{}",start);
for (int i=0 ;i<1000; i++) {
Thread t =new Thread(() ->{
window.sell(6);
sellCounts.add(6);
});
threadList.add(t);
t.start();
}
//等待所有买票线程跑完,再执行main线程中的方法
for ( Thread t: threadList) {
t.join();
}
long end = System.currentTimeMillis();
log.info("结束时间:{}",end);
log.info("耗时:{}",end - start);
log.info("卖出的票:{}",sellCounts.stream().mapToInt(i -> i).sum());
log.info("剩余的票:{}",window.getTickets());
}
}
执行结果:
23:34:30.523 [main] INFO com.learning.concurrent.SellTicketTest - 开始时间:1589816070521
23:34:31.612 [main] INFO com.learning.concurrent.SellTicketTest - 结束时间:1589816071612
23:34:31.612 [main] INFO com.learning.concurrent.SellTicketTest - 耗时:1091
23:34:31.615 [main] INFO com.learning.concurrent.SellTicketTest - 卖出的票:6000
23:34:31.615 [main] INFO com.learning.concurrent.SellTicketTest - 剩余的票:54582
线程不安全,因为票数加起来不等于60000.
1.2线程安全方式
package com.learning.concurrent;
/**
* ClassName:TicketWindow
* Package:com.learning.concurrent
* Desciption:
*
* @date:2020/5/18 22:33
* @author:
*/
public class TicketWindow {
private int tickets;
public TicketWindow(int tickets) {
this.tickets = tickets;
}
public synchronized void sell (int sells) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets - sells >=0) {
tickets = tickets -sells;
}
}
public int getTickets() {
return tickets;
}
}
这里在买票窗口中添加了synchronized ,并且休眠时间调成了10毫秒,执行结果如下:
23:38:35.179 [main] INFO com.learning.concurrent.SellTicketTest - 开始时间:1589816315178
23:38:45.940 [main] INFO com.learning.concurrent.SellTicketTest - 结束时间:1589816325940
23:38:45.940 [main] INFO com.learning.concurrent.SellTicketTest - 耗时:10762
23:38:45.944 [main] INFO com.learning.concurrent.SellTicketTest - 卖出的票:6000
23:38:45.944 [main] INFO com.learning.concurrent.SellTicketTest - 剩余的票:54000
重要结论:
这个是线程安全的,因为卖出的票+剩余的票等于60000.但是观察两种结果,第一种结果我睡眠1000毫秒,大约花了1秒时间执行完成,第二种结果我睡眠了仅仅10毫秒,却花了10s的时间执行完成,想象一下,当第1001个人来买票的时候,花了10s钟才进行响应,多可怕,如果10000个人同时访问呢?花费100s吗?这个太可怕了。
二、转账案例
2.1线程不安全的方式
package com.learning.concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
输出结果:
21:47:00.784 [main] DEBUG c.ExerciseTransfer - total:55
2.2线程安全的方式
上面的方式,共享变量有两个,一个是A账户的余额,一个是B账户的余额,如果说单纯的加synchronized关键字是不起作用的,因为只能锁住A对象或者B对象,而我们应该需要锁住A和B账户共享的对象即可解决,即锁住账户的class文件:
package com.learning.concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
输出结果:
21:47:42.392 [main] DEBUG c.ExerciseTransfer - total:2000
三、JAVA对象头
也可以参考 https://blog.csdn.net/SCDN_CP/article/details/86491792
以32位的虚拟机为例:
8个字节,其中4个字节是Mark Word。
普通对象:
数组对象:
其中Mark Word的结构为:
64位虚拟机的Mark Word:
四、Monitor原理
Monitor 被翻译为监视器或管程
Monitor是操作系统提供的对象
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下:
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一
个 Owner - 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
EntryList BLOCKED - Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
wait-notify 时会分析
注意:
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则