开头:
互斥锁是为了保证同一个方法同时间只有一个线程去执行,这个也是在多线程开发当中最基本的实现。在java体系当中有很多方法可以实现目的,如: synchronized ,lock ,redis分布式锁,zk分布式锁,基于数据库实现悲观锁等等。 本文将介绍synchronized这个java原生支持的互斥锁。
简易的锁模型
我们把一段需要互斥的代码称为:临界区。 当线程需要进入临界区的时候,线程需要去尝试获取锁,当获取成功之后就进去临界区执行代码,执行结束之后就去释放锁。 如果在获取线程的时候,其他线程正在使用锁,就会阻塞等待。
使用方法
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
这段代码示例中没有出现加锁和解锁的方法,他们的实现是由java编译器在synchronized修饰的方法或者代码块前后自动加上的。
synchronized使用时候有以下几条规则:
- 当修饰静态方法的时候,锁定的是当前类的Class对象,在上面的例子 就是Class X.
- 当修饰非静态方法的时侯,锁定的是当前实现对象this。
示例: 用synchronized解决 count+=1问题
MyCalc这个类有两个方法:一个是get()方法,用来获取value的值;另一个是addOne()方法,用来给value加1,并且addOne()方法我们用Synchronized修饰。
class SafeCalc {
long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
因为addOne()方法被synchronized修饰之后,这个方法就会同时只有一个线程去执行,所以一定能保证原子操作。因为我们的value方法是成员变化,会不会有线程可见性问题呢?
这里就会出来另一个概念: 管程与管程中锁的规则。
1. 管程: 就是我们这里的synchronized。
2. 管程中锁的规则: 对一个锁的解锁 Happens-before 做用于后续这个锁的加锁。
第二句话的意思是指 前一个线程的解锁操作对后一个线程的加锁操作是可见的,综合Happens-before的传递性规则,我们就得出前一个线程在临界区修改的共享变更(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。
按照这个规则,如果多个线程同时操作addOne()这个方法, 可见性是可以保证的,也就是说如果有100 个线程去执行addOne()这个方法,最终的value的结果会是增加了100。
但是此时还是有一个问题,get()我方法的可见性是无法保证的。管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而get()方法并没有加锁操作。所以可见性是无法保证的。我们可以通过给get() 方法加上synchronized修饰来保证可见性。
class SafeCalc {
long value = 0L;
synchronized long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
更改后的模型:
错误方式
package com.bdf.blog.thread.Synchronized;
/**
* @program: blog
* @description:
* @author: canghaihongxin
* @create: 2019-04-21 12:24
**/
public class SafeCacl {
static int value ;
public synchronized int getValue() {
return value;
}
public synchronized static void addOne(){
value +=1;
}
}
这样就是一个错误的代码示例, 当我们在addOne()方法上添加了static 关键字的时候就Synchronized锁的就是我SafeCacl.class这个类,也就是类锁。synchronized锁的是this。
这个时候getValue()方法就无法保证可见性了。