1. 线程共享带来的问题
1.1 线程安全问题
线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了。
代码示例
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0么?
@Slf4j(topic = "Test")
public class Test {
public static int cnt = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
cnt++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
cnt--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", cnt);
}
}
17:22:35.234 [main] DEBUG Test - 1049
结果不是0
- 字节码: ```java count++; // 操作字节码如下: getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i
count—; // 操作字节码如下: getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i
当 CPU 时间片分给 t1 线程时,t1 线程去读取变量值为 0 并且执行 ++ 的操作,如上在字节码自增操作中,当 t1 执行完自增,还没来得急将修改后的值存入静态变量时,假如线程的时间片用完了,并且 CPU 将时间片分配给 t2 线程,t2 线程拿到时间片执行自减操作,并且将修改后的值存入静态变量,此时 count 的值为 -1,但是当 CPU 将时间片分给经历了上下文切换的 t1 线程时,t1 将修改后的值存入静态变量,此时 counter 的值为 1,覆盖了 t2 线程执行的结果,出现了丢失更新,这就是多线对共享资源读取的问题。
- 正常情况下:
![截屏2021-04-29 下午5.26.34.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688397162-03a9407f-d5ed-427a-9e88-926d6d8cdeab.png#clientId=ufb484f9b-faa0-4&from=drop&id=ude4904c6&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.26.34.png&originHeight=639&originWidth=584&originalType=binary&size=78200&status=done&style=none&taskId=uc141cf09-28d5-436f-a604-734c205c489)
- 交错运行,出现负数:
![截屏2021-04-29 下午5.27.10.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688432379-0d9a1789-1e27-489e-ab8d-85634380303e.png#clientId=ufb484f9b-faa0-4&from=drop&height=565&id=u5c3496df&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.27.10.png&originHeight=718&originWidth=800&originalType=binary&size=104680&status=done&style=none&taskId=u98e2a423-0f0b-4502-99d6-7297690fc12&width=630)
- 交错运行,出现正数
![截屏2021-04-29 下午5.32.17.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619688741127-f78bc57e-f665-44b7-9a0b-ed34ca44e277.png#clientId=ufb484f9b-faa0-4&from=drop&height=593&id=u701cbd51&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-29%20%E4%B8%8B%E5%8D%885.32.17.png&originHeight=723&originWidth=694&originalType=binary&size=99606&status=done&style=none&taskId=u7795f3f2-3ece-4f5e-a07d-6ec4a3ca9b3&width=569)
<a name="KDujD"></a>
## 1.2 临界区**Critical Section**
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
例如,下面代码中的临界区
```java
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
1.3 竞态条件Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2.synchronized 解决方案
2.1 解决手段
为了避免临界区中的竞态条件发生,由多种手段可以达到。
- 阻塞式解决方案:synchronized ,Lock
- 非阻塞式解决方案:原子变量
现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
2.2 synchronized 语法
synchronized(对象) {
//临界区
}
2.3 synchronized使用
@Slf4j(topic = "Test")
public class Test {
public static int cnt = 0;
public static final Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object){
cnt++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object) {
cnt--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", cnt);
}
}
20:21:58.972 [main] DEBUG Test - 0
可以看到,加上锁之后,得到的数字是正确的。
- synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断。
- 问题:
- 如果把synchronized(obj)放在for循坏的外面,如何理解?
之前是对一个cnt++进行锁,其实是保证了字节码中的4行代码;对for循环加锁,就是保证了20000行字节码的原子性。
如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎样运作?
要保护共享资源,必须让多个线程锁同一个对象。
如果t1 synchronized(obj) 而 t2 没有加锁会怎样?
在t1加锁之后。t2尝试获取锁时不会被阻塞,也保护不了共享资源
- 使用面向对象的思维改进 ```java @Slf4j(topic = “Test”) public class Test {
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();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCnt());
}
}
class Room { public static int cnt = 0;
public void increment() {
synchronized (this) {
cnt++;
}
}
public void decrement() {
synchronized (this) {
cnt--;
}
}
public int getCnt() {
synchronized (this){
return cnt;
}
}
}
```java
20:45:46.608 [main] DEBUG Test - 0
2.4 方法上的synchronized
2.4.1 成员方法加锁
加在成员方法上,锁住的是对象 ```java public class Test { // 在方法上加上synchronized关键字 public synchronized void test() {
} // 等价于 public void test() {
synchronized(this) { // 锁住的是this对象
}
} }
<a name="xIsaw"></a>
### 2.4.2 静态方法加锁
- 加载静态方法上,锁住的是类
```java
public class Test {
// 在静态方法上加上 synchronized 关键字
public synchronized static void test() {
}
//等价于
public static void test() {
synchronized(Test.class) { // 锁住的是类对象
}
}
}
线程八锁
考察synchronized锁住的是哪个对象?
情况1: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
t1.start();
t2.start();
} }
@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ log.debug(“1”); }
public synchronized void b(){
log.debug("2");
}
}
结果:t1(n1)和t2(n1)锁住同一个对象(方法上,this), 互斥:
- 先1后2
- 先2后1
```java
23:10:12.127 [t1] DEBUG c.Test2 - begin
23:10:12.127 [t2] DEBUG c.Test2 - begin
23:10:12.133 [t2] DEBUG c.Number - 2
23:10:12.133 [t1] DEBUG c.Number - 1
23:13:28.607 [t1] DEBUG c.Test2 - begin
23:13:28.607 [t2] DEBUG c.Test2 - begin
23:13:28.610 [t1] DEBUG c.Number - 1
23:13:28.611 [t2] DEBUG c.Number - 2
情况2: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
t1.start();
t2.start();
} }
@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public synchronized void b(){
log.debug("2");
}
}
结果:t1(n1)与t2(n1)锁住同一个对象(方法上,this), 互斥:
- 休眠1s,先1后2
- 先2,休眠1秒,后1
```java
23:20:15.998 [t1] DEBUG c.Test2 - begin
23:20:15.998 [t2] DEBUG c.Test2 - begin
23:20:17.008 [t1] DEBUG c.Number - 1
23:20:17.009 [t2] DEBUG c.Number - 2
23:19:55.184 [t1] DEBUG c.Test2 - begin
23:19:55.184 [t2] DEBUG c.Test2 - begin
23:19:55.188 [t2] DEBUG c.Number - 2
23:19:56.192 [t1] DEBUG c.Number - 1
情况3: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
Thread t3 = new Thread(()-> {
log.debug("begin");
n1.c();
}, "t3");
t1.start();
t2.start();
} }
@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public synchronized void b(){
log.debug("2");
}
public void c(){
log.debug("3");
}
}
结果:t1(n1)和t2(n2)锁住同一个对象(方法上,this),互斥执行;3没有加synchronized,并行执行
- 先3,休眠1s后1与2
- 先3与2,休眠1s后1
- 先2与3,休眠1s后1
```java
23:27:32.560 [t1] DEBUG c.Test2 - begin
23:27:32.560 [t2] DEBUG c.Test2 - begin
23:27:32.560 [t3] DEBUG c.Test2 - begin
23:27:32.565 [t3] DEBUG c.Number - 3
23:27:33.568 [t1] DEBUG c.Number - 1
23:27:33.569 [t2] DEBUG c.Number - 2
23:26:48.360 [t1] DEBUG c.Test2 - begin
23:26:48.360 [t3] DEBUG c.Test2 - begin
23:26:48.360 [t2] DEBUG c.Test2 - begin
23:26:48.363 [t3] DEBUG c.Number - 3
23:26:48.363 [t2] DEBUG c.Number - 2
23:26:49.368 [t1] DEBUG c.Number - 1
23:27:11.653 [t1] DEBUG c.Test2 - begin
23:27:11.653 [t3] DEBUG c.Test2 - begin
23:27:11.653 [t2] DEBUG c.Test2 - begin
23:27:11.656 [t2] DEBUG c.Number - 2
23:27:11.656 [t3] DEBUG c.Number - 3
23:27:12.659 [t1] DEBUG c.Number - 1
情况4: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
t1.start();
t2.start();
}
}
@Slf4j(topic = “c.Number”) class Number { public synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public synchronized void b(){
log.debug("2");
}
}
结果:t1(n1)和t2(n2)锁的是不同的对象(加在方法上,this),并行执行
- 先2,休眠1s后1
```java
23:31:53.335 [t2] DEBUG c.Test2 - begin
23:31:53.335 [t1] DEBUG c.Test2 - begin
23:31:53.339 [t2] DEBUG c.Number - 2
23:31:54.344 [t1] DEBUG c.Number - 1
情况5: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
t1.start();
t2.start();
}
}
@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public synchronized void b(){
log.debug("2");
}
}
结果:t1加了static,锁的是Number类对象;t2锁的还是this对象(n1),不同的对象,并行
- 先2,休眠1s后1
```java
23:37:52.532 [t1] DEBUG c.Test2 - begin
23:37:52.532 [t2] DEBUG c.Test2 - begin
23:37:52.536 [t2] DEBUG c.Number - 2
23:37:53.538 [t1] DEBUG c.Number - 1
情况6: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n1.b();
}, "t2");
t1.start();
t2.start();
}
}
@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public static synchronized void b(){
log.debug("2");
}
}
结果:t1与t2都加了static,锁的是同一个Number对象,互斥,结果同情况2
- 休眠1s,先1后2
- 先2,休眠1秒,后1
- 情况7:
```java
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, "t1");
Thread t2 = new Thread(()-> {
log.debug("begin");
n2.b();
}, "t2");
t1.start();
t2.start();
}
}
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a(){
try {
Thread.sleep(1000);
log.debug("1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void b(){
log.debug("2");
}
}
结果:t1加了static锁Number;t2是this,锁n2,对象不同,并行。与情况4一样
先2,休眠1s后1
- 情况8: ```java @Slf4j(topic = “c.Test2”) public class Test2 {
public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); Thread t1 = new Thread(()-> {
log.debug("begin");
n1.a();
}, “t1”);
Thread t2 = new Thread(()-> {
log.debug("begin");
n2.b();
}, “t2”);
t1.start();
t2.start();
}
}
@Slf4j(topic = “c.Number”) class Number { public static synchronized void a(){ try { Thread.sleep(1000); log.debug(“1”); } catch (InterruptedException e) { e.printStackTrace(); }
}
public static synchronized void b(){
log.debug("2");
}
}
结果:t1与t2都是static,锁的是同一个对象(Number),互斥,与结果2相同。
- 休眠1s,先1后2
- 先2,休眠1秒,后1
<a name="R2CAm"></a>
## 2.5 变量的线程安全分析
<a name="Rrdgq"></a>
### 2.5.1 成员变量和静态变量
- 如果变量没有在线程间共享,那么线程对该变量操作是安全的
- 如果变量在线程间共享
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码就是临界区,需要考虑线程安全问题
<a name="ue3ap"></a>
### 2.5.2 局部变量线程安全分析
- 局部变量【局部变量被初始化为基本数据类型】是安全的
- 局部变量是引用类型或者是对象引用则未必是安全的
- 如果局部变量引用的对象没有引用线程共享的对象,那么是线程安全的
- 如果局部变量引用的对象引用了一个线程共享的对象,那么要考虑线程安全问题
<a name="HgX8D"></a>
### 2.5.3 局部变量线程安全分析
- 局部变量本身
```java
public static void test() {
int i = 10;
i++;
}
每个线程调用test()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
字节码:只有第3行一条指令
局部变量引用 ```java @Slf4j(topic = “c.Code_18_Test”) public class Code_18_Test {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for(int i = 0; i < 10; i++) {
new Thread(() -> {
unsafeTest.method1();
}, "t" + i).start();
}
}
}
class UnsafeTest {
List<Integer> list = new ArrayList<>();
public void method1() {
// 临界区
for (int i = 0; i < 200; i++) {
method2();
method3();
}
}
private void method2() {
list.add(1);
}
private void method3() {
list.remove(0);
}
}
实例变量list是被共享的,其中一种情况是,线程2还没有add,线程3就romove了,没有数据是没办法remove的。<br />![截屏2021-04-30 上午9.35.51.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619746555730-b2377242-6b59-4052-ae0d-575da0e41fcc.png#clientId=u331f982e-2d25-4&from=drop&id=u5407e0dd&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-04-30%20%E4%B8%8A%E5%8D%889.35.51.png&originHeight=686&originWidth=900&originalType=binary&size=311052&status=done&style=none&taskId=u75ff7a1a-9974-4eb8-8a7e-b3f0269582a)
- 将list修改成局部变量,没有发生逃逸
```java
class SafeTest {
public void method1() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 200; i++) {
method2(list);
method3(list);
}
}
private void method2(List<Integer> list) {
list.add(1);
}
private void method3(List<Integer> list) {
list.remove(0);
}
}
可以将 list 修改成局部变量,然后将 list 作为引用传入方法中,因为局部变量是每个线程私有的,会创建不同的实例,不会出现共享问题。而method2或method3的参数是从method1中传递过来的,与method1中引用同一个对象。
- 局部变量,发生逃逸
将mehtod2和method3方法改成public
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
情况1:有其它线程调用method2和method3,不会引发线程安全问题,因为直接调用method2传入的对象不是method1中的对象。
情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2和method3方法
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
存在线程安全问题,list被method1中的线程和子类重写的method3的线程共享了。
2.6 常见线程安全的类
- String
- Integer
- StringBuffer
- Random
- Vector (List的线程安全实现类)
- Hashtable (Hash的线程安全实现类)
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
可以理解为:
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
2.6.2 不可变类的线程安全
String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的,有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了!
源码:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen); // 新建一个对象,然后返回,没有修改等操作,是线程安全的。
}
2.7 实例分析
分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。
例1:
public class MyServlet extends HttpServlet{
Map<String, Object> map = new HashMap<>(); // 线程不安全
String s1 = "..."; // 线程安全,字符串不可变
final String s2 = "..."; // 线程安全
Date d1 = new Date(); // 线程不安全
final Date d2 = new Date(); // 线程不安全,日期内的属性时可变的
public void doGet(HttpServletRequest request, HttpServletResponse response){
// 使用上述变量
}
}
例2:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl(); // 线程不安全,count被共享
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
//...
count++;
}
}
例3:
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L; // 线程不安全,单例的会被共享,start会被多个线程同时读写操作
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
例4:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl(); //线程安全,同理
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl(); // 线程安全,虽然会被共享,但是UserDaoImpl是线程安全的(没有可更改的东西,不可变)
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {// 线程安全,没有成员变量
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ... //线程安全,connection是局部变量,每个线程独一份
} catch (Exception e) {
// ...
}
}
}
例5:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl(); //线程不安全,同理
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl(); // 线程不安全,UserDaoImpl不安全了
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 线程不安全,成员变量被共享了,出现多个读写操作
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
例6
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 UserDao是作为局部变量存在的,所以每个对象会创建新的UserDao,独有的,所以是线程安全的
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
3. Monitor概念
3.1 Java对象头
以 32 位虚拟机为例,普通对象的对象头结构如下,其中的 Klass Word 为指针,指向对应的 Class 对象;
- 普通对象(64位的话markword是12字节,KClass是4字节)
- 数组对象
- Mark Word结构为
3.2 Monitor工作原理
Monitor 被翻译为监视器或者说管程
每个 java 对象都可以关联一个 Monitor ,如果使用 synchronized 给对象上锁(重量级),该对象头的 Mark Word 中就被设置为指向 Monitor 对象的指针。
- 刚开始时 Monitor 中的 Owner 为 null
- 当 Thread-2 执行 synchronized(obj){} 代码时就会将 Monitor 的所有者Owner 设置为 Thread-2,上锁成功,Monitor 中同一时刻只能有一个 Owner
- 当 Thread-2 占据锁时,如果线程 Thread-3 ,Thread-4 也来执行synchronized(obj){} 代码,就会进入 EntryList(阻塞队列) 中变成BLOCKED(阻塞) 状态
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
字节码角度
public class Test3 {
private static Object lock = new Object();
private static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
}
0 getstatic #2 <com/ll/ch3/Test3.lock> // lock引用
3 dup
4 astore_1
5 monitorenter // 将lock对象markword置为monitor指针
6 getstatic #3 <com/ll/ch3/Test3.counter>
9 iconst_1
10 iadd
11 putstatic #3 <com/ll/ch3/Test3.counter>
14 aload_1
15 monitorexit // 将lock对象markword重置,唤醒了EntryLIst
16 goto 24 (+8)
19 astore_2 // 异常部分
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
4. synchronized 原理进阶
4.1 轻量级锁
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。
- 轻量级锁对使用者是透明的,即语法仍然是 synchronized 。
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存将来要存储对象的 Mark Word 和对象引用 reference
- 让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中。
- 如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态 00 (表示轻量级锁),如下所示。(替换成功的条件是object对象的锁状态之前是01无锁)
- 如果cas失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
- 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数。
- 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,
- 即为对象申请Monitor锁,让Object指向重量级锁地址,此时Object对象头中的锁状态标志为10(重量级锁)
- 然后自己进入Monitor 的EntryList 变成BLOCKED状态
- 当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头,对象的对象头指向 Monitor,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 Thread-1 线程
[
](https://blog.csdn.net/weixin_50280576/article/details/113033975)
4.3 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁
- 自旋重试成功的情况
- 自旋重试失败的情况
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
4.4 偏向锁
在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行 CAS 操作。那java6 开始引入了偏向锁,只有第一次使用 CAS 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。以后只要不发生竞争,这个对象就归该线程所有。 ```java static final Object obj = new Object(); public static void m1() { synchronized(obj) {
} } public static void m2() { synchronized(obj) {// 同步块 A
m2();
} } public static void m3() { synchronized(obj) {// 同步块 B
m3();
} }// 同步块 C
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12943861/1619763179405-d8b2ef6b-396a-4003-b510-963a2110d405.png#clientId=u331f982e-2d25-4&from=paste&height=789&id=u01b46b6b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1045&originWidth=990&originalType=binary&size=312535&status=done&style=none&taskId=uaa9e4fc1-d899-4963-b20c-901efbbb002&width=747)
<a name="v3Ajc"></a>
### 4.4.1 偏向状态
一个对象的创建过程
- 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的 Thread,epoch,age 都是 0 ,在加锁的时候进行设置这些的值.
- 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
-XX:BiasedLockingStartupDelay=0 来禁用延迟
- 禁用偏向锁: -XX:UseBiasedLocking。如果没有开启偏向锁,那么对象创建后,markword的最后三位置为001,这时它的hashcode、age都为0,第一次用到hashcode时才赋值
- 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
<a name="LNNkx"></a>
### 4.4.2 撤销偏向
以下几种情况会使对象的偏向锁失效
- 调用对象的 hashCode 方法
- 多个线程使用该对象
- 调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁)
<a name="rzkgr"></a>
### 4.4.3 批量重偏向
- 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 t1 的对象仍有机会重新偏向 t2
- 重偏向会重置Thread ID
- 当撤销超过20次后(超过阈值,第20次开始),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至该加锁线程。
<a name="S1eCW"></a>
### 4.4.4 批量撤销
当撤销偏向锁的阈值超过 40(第40次开始) 以后,就会将**整个类的剩下的对象都改为不可偏向的,新建的对象也是不可偏向的**<br />总结:
- 当线程1对40个对象加偏向锁(101)后,存储该线程1的id,线程2重新给这40个对象加锁,之后线程3再个这40个对象加锁
- 线程2:
- 前19个对象为撤销偏向,线程2加锁后由原先线程1的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);
- 从第20个对象开始,批量重偏向,线程2加锁后由原先的线程1的偏向锁变为该线程的偏向锁(101),存储该线程2的id,同步代码块执行完成后状态不变;
- 线程3:
- 前19个对象已经是正常状态,执行加轻量锁(00)和释放锁(001)的。(该对象偏向锁已经失效)
- 从第20个对象开始,撤销偏向,线程3加锁后由原先线程2的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);
- 从第40个对象开始,剩下的对象会撤销偏向,由原先线程2的偏向锁升级为轻量级锁(00),同步代码块执行完成后变为正常状态(001);新建的对象会进入正常状态(001)
<a name="gNKiV"></a>
## 4.5 锁消除
如果该加锁的对象是一个局部变量的话(没发生逃逸不会引起线程安全问题),该锁会自动消除。加不加都一样,加了会影响性能。
```java
public void b() {
Object o = new Object();
synchronized(o){
x++;
}
}
该锁会自动消除,对象o是一个局部变量且没有逃逸,不会引起线程安全,锁被优化掉了。
- 关闭锁消除:-XX:-EliminateLocks