1、案例引发的思考:多线程环境下不加锁计算累加和
static int i = 0;
public static void incr() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
public static void main(String[] args) throws InterruptedException {
for (int index = 0; index < 1000; index++) {
new Thread(() -> {
SyncDemo.incr();
}).start();
}
Thread.sleep(5000);
System.out.println(i);
}
输出结果:
1000个线程去对一个共享变量累加,得到的结果是一个小于等于1000的值。这是线程切换导致的结果。
i++ 的底层是一组指令:\
拿到i的值
i+1
给i赋值
多线程去操作i++的时候,可能线程A这一组指令没有执行完,然后发生了线程切换,线程B又去执行,执行完让出CPu时间片给到A,此时A读取到自己工作内存中的值还是线程B未加之前的,这种情况就会少加一次1然后返回,最终我们看到的结果就是小于等于1000。\
2、原子性
线程安全问题:原子性、有序性、可见性
原子性:一组操作要么全部成功执行要么全部不执行
有序性:一个线程执行的一组指令是按顺序执行的
可见性:一个线程对共享变量的修改要对其他线程可见
要解决上面i计算结果小于等于1000这个问题,就是要保证i++这一组操作的原子性,一个线程在执行i++没有完成的时候不让其他线程进来。我们可以使用synchronized关键字来保证。
给i++操作加锁
输出结果:
3、synchronized的用法
synchronized锁住的是什么? 是共享资源。
sychronized的锁对象可以是类对象,也可以是对象
修饰静态方法的锁对象是类对象
也可以修饰代码块,这里的锁对象也是类对象
也可以使用类的实例也就是对象作为锁对象
使用synchronized的关键就是要让多个线程竞争同一把锁,才能保证原子性,要是1000个线程去竞争1000个锁,就失去了锁的作用,所以这里使用的时候要特别注意锁对象的作用域
3、synchronized的原理
锁的存储
对象在堆中布局分为对象标记、类元信息、实例数据、对齐填充四个部分, 对象标记中存储着hashcode、分代年龄、锁的标记等消息。
锁的升级
偏向锁->轻量级锁->重量级锁
偏向锁:线程A进来判断对象头中是否有其他线程id,如果没有,通过一次CAS操作设置对象头的线程id为自己的线程id;此时线程B进来,也进行同样的操作,发现对象头中已经有线程A的id,尝试撤销锁,撤销成功就把对象头的线程id设置为自己,只有达到全局安全点的时候才能撤销成功,撤销失败就升级为轻量级锁。
轻量级锁:假设线程A抢占到轻量级锁,就在自己的线程栈帧中维护一个Lock Record,然后将对象头中维护一个指针指向Lock Record;线程B进来也进行通过的操作,修改指针失败之后通过多次CAS(自旋)去尝试获取锁,如果获取锁失败就升级为重量级锁。
重量级锁:假设线程A执行monitorenter指令,抢占到重量级锁,此时线程B进来,进入同步队列,如果是执行了wait方法就进入等待队列,等待抢占到锁的线程执行monitorexit指令然后自己被唤醒。
4、生产者消费者模型
创建一个生产者生产消息,队列满了就阻塞,等待被唤醒;创建一个消费者消费消息,队列空了就阻塞,等待被唤醒。
生产者代码:
public class Product implements Runnable{
private Queue<String> msg;
private Integer size;
public Product(Queue<String> msg, Integer size) {
this.msg = msg;
this.size = size;
}
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
int i = 0;
while (true) {
synchronized (msg) {
i++;
while (msg.size() == size) {
try {
msg.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者生产消息," + i);
msg.add("生产消息" + i);
msg.notify();
}
}
}
}
消费者代码:
public class Consumer implements Runnable {
private Queue<String> msg;
private Integer size;
private static int i = 0;
public Consumer(Queue<String> msg, Integer size) {
this.msg = msg;
this.size = size;
}
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
while (true) {
synchronized (msg) {
while (msg.isEmpty()) {
try {
msg.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者消费消息" + msg.remove());
msg.notify();
}
}
}
}
测试方法:
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
Product product = new Product(queue, 5);
Consumer consumer = new Consumer(queue, 5);
Thread thread1 = new Thread(product);
Thread thread2 = new Thread(consumer);
thread1.start();
thread2.start();
}
wait和nofity也是基于重量级锁去实现的,抢占的必须是同一把锁。