共享问题

  1. i++ 不是一个原子的操作
    需要先从主内存读取值,然后在线程中+1,然后再把值写回主内存

临界区

多个线程对共享资源读写操作法发生指令交错,就会出现问题

线程安全的问题一般都是由存在临界区的代码导致的

一个代码块内如果对共享变量操作的代码块叫做临界区

竞态条件

在临界区存在并发操作一个资源

synchronized

应用互斥, 防止临界区的竞态条件的发生, 可以有多个手段达到目的

  • 阻塞方式
    • synchronized
    • Lock
  • 非阻塞
    • 原子变量

如何使用?
直接将一个共享对象作为一个锁对象,注意如果一个线程拿到了锁之后,时间片用完了,并不会释放锁, 其他线程仍然得等这个线程重新获得时间片,然后释放锁之后,才能重新获取锁

用对象锁保证了临界区代码的原子性, 不会被其他的打断, 不会被打断
必须要多个线程的临界区都需要使用同一个对象加锁, 否则等于没加

1
2
3
4
5
6
7
8
9
10
synchronized (synchronized_demo.class) {
for (int i = 0; i < 100000; i++) {
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
a++;
}
}

面向对象的改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class synchronized_demo {
public static void main(String[] args) throws InterruptedException {

room room = new room();

Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i ++) {
room.increment();
}
});

Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i ++) {
room.decrement();
}
});

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(room.getCount());

}

}


class room {
private int count = 0;

public void increment() {
synchronized (this) {
count++;
}
}
public void decrement() {
synchronized (this) {
count--;
}
}

public int getCount() {
synchronized (this) {
return count;
}
}

}

方法上的synchronized

等价与锁自己所在的类的实例

锁静态方法和锁动态方法

1
2
3
4
5
class Test{
public synchronized void test() {
//
}
}

等价于

1
2
3
4
5
6
7
class Test{
public void test() {
synchronized(this) {
//
}
}
}

锁静态方法等价于锁类的class对象

线程安全分析

  • 没有共享就线程安全
  • 如果被共享了
    • 如果是只读操作,也安全
    • 如果有读写操作, 临界区需要考虑线程安全问题

栈线程私有, 堆线程共享

如果线程的局部变量的引用暴露给了其他的线程, 其他的线程可以通过引用访问这个变量, 也会造成线程不安全
可以使用访问修饰符, 以及final 不可集成

开闭原则, 向子类关闭, 防止子类继承之后, 重写方法, 新建线程,暴露引用导致线程不安全

这里的线程安全是指: 多个线程调用他们的同一个实例的某一个方法的时候, 也是线程安全的

spring中的对象都是单例的, 直接使用线程不安全 需要使用方法中的局部变量

防止外星方法 使用final

Monitor(管程)

java对象头

普通对象头:

mark word / klass word

mark word 结构

普通状态

hashcode 25 age 4 biased_lock 0(偏向锁) | 01 (加锁状态)

数组对象头 加上一个数组长度

monitor详解

当对象被syn锁之后, 就会指向一个monitor的指针

monitor是一个os级的对象, 在jvm中无法表示

加锁过程

monitor由三个部分组成: WaitSet, EntryList, Owner

当一个对象被上锁之后, 首先会把对象的对象头的markword设置成指向monitor的指针, 然后线程指向Owner (通过owner关联)

另一个线程在获取锁的时候, 首先检查这个对象的markword, 如果这个markword已经指向monitor, 就说明已经上锁, 这个时候将挂载entryList(阻塞队列)后面, 同时进入阻塞

解锁之后, 再把markword 恢复

注意一个对象关联一个monitor, 必须要锁同一个对象才会有效果

轻量级锁

一个对象, 虽然有多线程访问, 但是多线程访问的是错开的, 可以使用轻量级锁 (如果有竞争, 会直接升级成重量级锁)

轻量级锁对于使用者是透明的, 直接使用synchronized即可

wait/notify

线程状态转换

活跃性

Lock