java锁相关之synchronized关键字概念与使用
JAVA中锁的概念
- 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够被成功获取,知道获取到锁才会推出循环;
- 乐观锁:假定没有冲突,在修改数据时如果发现要修改的数据和之前获取的不一致,则重新读取数据,修改后重新尝试修改;
- 悲观锁:假定会发生冲突,同步所有对数据的相关操作,从读数据就开始上锁;
- 独享锁(写):给资源上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
- 共享锁(读):给资源上读锁后,只能读不能改,其他线程只能加读锁,不能加写锁;(多读)
- 可重入锁与不可重入锁:线程拿到一把锁后,可以自由进入同一把锁所同步的其他代码
- 公平锁与非公平锁:争取锁的顺序,如果先来后到,则为公平
同步关键字synchronized
相关概念
- 用于实例方法、静态方法,隐式制定锁对象
- 用于代码块时,显示制定锁对象
- 锁的作用于:对象锁、类锁
- 特性:synchronized是一个可重入、独享、悲观锁
- 锁优化:
- 锁消除:(开启锁消除的参数:-XX:+DoEscapeAnalysis -XX:+EliminateLocks)–一般是在循环体或者频繁调用的时会触发JIT编译,进行锁消除,例如:频繁使用StringBuffder.append时
- 锁粗化:JVM做了锁粗化的优化,但是我们可以从代码层进行优化
栗子
对实例方法的锁定,锁定的是实例对象,当相同对象调用两个方法是,会进行锁定,如果有两个对象同时调用,则不会锁定
public synchronized void update() {
...
}
等价于
public void update() {
synchronized(this){
...
}
}
测试示例:
class Counter {
// synchronized (this){}
public synchronized void update() {
System.out.println("访问数据库1");
try {
for (int i = 1; i < 4; i++) {
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】锁住了" + i + "秒");
Thread.sleep(1000);
}
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】释放了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void updateBlock() {
synchronized (this) {
System.out.println("访问数据库2");
try {
for (int i = 1; i < 4; i++) {
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】锁住了" + i + "秒");
Thread.sleep(1000);
}
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】释放了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
此时使用的是两个实例进行测试
public static void main(String args[]) {
Counter ct1 = new Counter();
Counter ct2 = new Counter();
new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("T1 method");
ct1.update();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("T2 method");
ct2.updateBlock();
}
}).start();
}
输出结果为:
访问数据库1
啊,我被【T1 method】锁住了1秒
访问数据库2
啊,我被【T2 method】锁住了1秒
啊,我被【T1 method】锁住了2秒
啊,我被【T2 method】锁住了2秒
啊,我被【T1 method】锁住了3秒
啊,我被【T2 method】锁住了3秒
啊,我被【T1 method】释放了
啊,我被【T2 method】释放了
将ct2更换为ct1实例后,此时会对实例进行锁定,在T1线程结束后,T1获取的锁会被释放,此时才会被T2线程获取
输出结果为:
访问数据库1
啊,我被【T1 method】锁住了1秒
啊,我被【T1 method】锁住了2秒
啊,我被【T1 method】锁住了3秒
啊,我被【T1 method】释放了
访问数据库2
啊,我被【T2 method】锁住了1秒
啊,我被【T2 method】锁住了2秒
啊,我被【T2 method】锁住了3秒
啊,我被【T2 method】释放了
对静态方法加锁,属于对类进行加锁,多个实例访问方法依然会被锁住
public static synchronized void update() {
...
}
等价于
public static void update() {
synchronized(Counter.class){
...
}
}
测试示例:
class Counter {
// synchronized (Counter.class)
public static synchronized void staticUpdate() {
System.out.println("访问数据库3");
try {
for (int i = 1; i < 4; i++) {
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】锁住了" + i + "秒");
Thread.sleep(1000);
}
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】释放了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void staticUpdateBlock() {
synchronized (Counter.class) {
System.out.println("访问数据库4");
try {
for (int i = 1; i < 4; i++) {
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】锁住了" + i + "秒");
Thread.sleep(1000);
}
System.out.println("啊,我被【" + Thread.currentThread().getName() + "】释放了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) {
Counter ct1 = new Counter();
Counter ct2 = new Counter();
new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("T3 method");
ct1.staticUpdate(); // 此处为了方便与之前的实例方法比较,实际应该用Counter.staticUpdate();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("T4 method");
ct2.staticUpdateBlock(); // 此处为了方便与之前的实例方法比较,实际应该用Counter.staticUpdate();
}
}).start();
}
输出结果为:
访问数据库3
啊,我被【T3 method】锁住了1秒
啊,我被【T3 method】锁住了2秒
啊,我被【T3 method】锁住了3秒
啊,我被【T3 method】释放了
访问数据库4
啊,我被【T4 method】锁住了1秒
啊,我被【T4 method】锁住了2秒
啊,我被【T4 method】锁住了3秒
啊,我被【T4 method】释放了
总结:
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;
- 如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。