java中对全局变量的操作是通过JMM(java内存模型)内存模型实现的,全局变量保存在主存中,但是变量的计算则是在线程的工作内存中。
如果对变量的操作不是原子操作(比如i++是由多条指令的操作集合)那么就会带来线程安全问题。
比如下面的i++和i++操作,结果可能就不是0。
static int count = 0;
@Test
public void testJMM() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
count++或者count–不是原子操作,可能会在中间的某条指令还没执行完时时间片却用完了,导致写入主内存数据的顺序不一致。
46 getstatic #46
49 iconst_1
50 iadd
51 putstatic #46
多个线程在临界区内执行,由于代码的执行序列不同(比如指令交错执行)而导致结果无法预测,称之为发生了 竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
synchronized实际上是用 对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
public static void main(String[] args) {
int threadCount = 2;
int loop = 200;
ThreadUnsafe threadUnsafe = new ThreadUnsafe();
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
for(int j=0; j < loop; j++) {
threadUnsafe.method();
}
}, "t" + i);
thread.start();
}
}
static class ThreadUnsafe {
private List<String> arr = new ArrayList<>();
public void method1() {
addE();
removeE();
}
private void method2() {
arr.add("1");
}
private void method3() {
arr.remove(0);
}
}
执行结果:
com.study.concurrentprogramming.practice.threadCommon
Exception in thread "t0" Exception in thread "t1"
at java.util.ArrayList.remove(ArrayList.java:505)
at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.removeE(threadCommon.java:38)
at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.method(threadCommon.java:30)
at com.study.concurrentprogramming.practice.threadCommon.lambda$main$0(threadCommon.java:18)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -2
at java.util.ArrayList.remove(ArrayList.java:505)
at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.removeE(threadCommon.java:38)
at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.method(threadCommon.java:30)
at com.study.concurrentprogramming.practice.threadCommon.lambda$main$0(threadCommon.java:18)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
导致失败的原因是增加元素不是原子操作,存在指令交错执行,线程0和线程1分别执行了add操作后,可能只对成员列表增加了一个元素,两个线程分别删除时导致数组越界。
public static void main(String[] args) {
int threadCount = 2;
int loop = 200;
ThreadSafe threadUnsafe = new ThreadSafe();
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
for(int j=0; j < loop; j++) {
threadUnsafe.method();
}
}, "t" + i);
thread.start();
}
}
static class ThreadSafe {
public void method1() {
List<String> arr = new ArrayList<>();
addE(arr);
removeE(arr);
}
private void method2(List<String> arr) {
arr.add("1");
}
private void method3(List<String> arr) {
arr.remove(0);
}
}
String、Integer等都是 不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
每个不可变类的状态都是固定的,因为是不变的,所以一个线程一旦创建了不可变类,那么不同的线程只是读取它们的状态而不会改变它们的状态,所以是线程安全的。
以32为虚拟机为例:
Integer对象保护:8字节对象头 + 4字节数据;int 变量只有4字节数据。
普通 对象:
|===============================================================================================================|
| Object Header(64bits) |
|==================================================|============================================================|
| Mark Word(32bits) | Klass Word(32bits) 指向方法区的Class对象 |
|===================================================|===========================================================|
其中Mark Word结构为:
|===================================================================================|===========================|
| Mark Word(32bits) |State(不同状态下的markwork值)|
=====================================================================================|===========================|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|====================================================================================|===========================|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|====================================================================================|===========================|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|====================================================================================|===========================|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|====================================================================================|===========================|
| | 11 | Marked for GC |
|====================================================================================|===========================|
Monitor被翻译成监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针,Monitor是 操作系统的对象非java中管理的对象,synchronized给对象上锁的过程就是把对象头的Mark Word指向操作系统的一个Monitor对象。
Monitor结构如下:
注意:
- synchronized必须是进入同一个对象的monitor才有上述的效果。
不加synchronized的对象不会关联监视器,不遵从以上规则。
在java6之前synchronized获取的对象是直接关联操作系统的Monitor锁的,但是Monitor是操作系统提供的,成本比较高,某些场景会降低程序的性能。为了解决这个问题,java6开始对synchronized获取锁的逻辑做了优化。
首先通过一个小故事来形象的感知一下synchronized优化后获取锁的原理。
故事主角:
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者透明,语法还是synchronized。
假如有两个方法同步块,利用同一个对象加锁。
public class sync {
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块
}
}
}
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。