前言
在阿里开发手册的并发处理模块有一个需要强制执行的要求:
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就 不要锁整个方法体;能用对象锁,就不要用类锁。
那什么叫无锁数据结构、什么叫锁、什么叫锁区块、什么叫锁对象、什么叫类锁呢?
本篇文章通过13个实例由浅入深带大家了解。
实例一
LockDemoOne类有两个实例方法d1(睡眠1秒)和d2,d1方法加锁锁住的是整个方法体,d2未加锁。
创建两个线程,两个线程公用同一个实例,调用不同的方法,查看是d1还是d2先执行
代码:
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* 锁Demo
*
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoOne {
public synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoOne demo = new LockDemoOne();
new Thread(() -> demo.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo.d2(), "t2").start();
}
}
效果:
d2线程名称:t2
d1线程名称:t1
结论:d1方法加锁,d2方法未加锁,最终d2先执行,是因为d1睡眠1秒导致,和锁无关
实例二
和实例一不一样的地方就是在LockDemoOneTwo中声明了一个实例变量作为锁的对象,锁的是区块,执行结果和实例一一样。
代码:
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* 锁Demo
*
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoOneTwo {
Object lock = new Object();
public void d1() {
synchronized (lock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoOneTwo demo = new LockDemoOneTwo();
new Thread(() -> demo.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo.d2(), "t2").start();
}
}
效果:
d2线程名称:t2
d1线程名称:t1
结论:d1方法加锁,d2方法未加锁,最终d2先执行,是因为d1睡眠1秒导致,和锁无关
实例三
相比实例将d2方法也加上synchronized关键字,执行效果却和实例一不一致。
代码:
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoTwo {
public synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public synchronized void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoTwo demo = new LockDemoTwo();
new Thread(() -> demo.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo.d2(), "t2").start();
}
}
效果:d2等待着d1执行完才执行
d1线程名称:t1
d2线程名称:t2
结论:实例方法上增加synchronized关键字,锁住的是demo这个对象 。如果类中有一个或者多个synchronized关键字修饰的方法,同一时间有且只能有一个线程拿到这个锁对象并且成功访问响应的方法。
实例四:
为了巩固实例三,我们增加一个实例执行效果和实例三一致。
代码:
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoTwoTwo {
Object lock = new Object();
public void d1() {
synchronized (lock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public void d2() {
synchronized (lock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoTwoTwo demo = new LockDemoTwoTwo();
new Thread(() -> demo.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo.d2(), "t2").start();
}
}
效果:
d1线程名称:t1
d2线程名称:t2
结论:实例三的锁对象是 LockDemoTwo类的实例对象,实例四的锁对象是Object,虽然锁对象不一致,但是实现效果是一致的。如果换成两个对象呢?咱们看看实例五和实例六
实例五
还是两个实例方法,都增加了synchronized关键字,锁住实例方法。但是两个线程用中用的是不同的实例对象。
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoThree {
public synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public synchronized void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoThree demo1 = new LockDemoThree();
LockDemoThree demo2 = new LockDemoThree();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo2.d2(), "t2").start();
}
}
效果:
d2线程名称:t2
d1线程名称:t1
结论:synchronized关键字修饰的方法,只能锁住同一个实例对象不同线程调用。如果是多个线程调用不同实例对象的 synchronized修饰方法互不影响。因为线程获取的锁对象改变了。
实例六
同实例五效果,因为获取的锁对象变了,所以调用互不影响
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoThreeTwo {
Object lock = new Object();
public void d1() {
synchronized (lock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public void d2() {
synchronized (lock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoThreeTwo demo1 = new LockDemoThreeTwo();
LockDemoThreeTwo demo2 = new LockDemoThreeTwo();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo2.d2(), "t2").start();
}
}
实例七
在实例五的基础上稍微调整:将d1方法增加static关键字修饰 , d1方法变成了静态类方法。
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoFour {
public static synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public synchronized void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoFour demo1 = new LockDemoFour();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo1.d2(), "t2").start();
}
}
d2线程名称:t2
d1线程名称:t1
结论:类方法和实例方法同样被synchronized修饰,但是锁住的对象却不是一个。类实例方法锁住的是当前实例对象(this), 类静态方法锁住的是Class<LockDemoFour>对象,因为不是一个锁对象所以执行互不影响。
实例八
和实例七执行效果一致
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoFourTwo {
static Object classLock = new Object();
Object instanceLock = new Object();
public void d1() {
synchronized (classLock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public void d2() {
synchronized (instanceLock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoFourTwo demo1 = new LockDemoFourTwo();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo1.d2(), "t2").start();
}
}
实例九
将两个方法都增加 static synchronized 关键字修饰,变成类静态方法,看看执行效果
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoFive {
public static synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public static synchronized void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoFive demo1 = new LockDemoFive();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo1.d2(), "t2").start();
}
}
d1线程名称:t1
d2线程名称:t2
结论:两个线程执行有先后顺序。因为静态方法锁住的是Class实例对象,咱们知道每个类有且仅有一个Class实例(同一个ClassLoader下) ,所以两个线程拿到的是同一把锁,需要按照拿到锁的先后顺序执行。
实例十
实例九的变形,执行结果一致
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoFiveTwo {
static Object classLock = new Object();
public static void d1() {
synchronized (classLock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public static void d2() {
synchronized (classLock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoFiveTwo demo1 = new LockDemoFiveTwo();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo1.d2(), "t2").start();
}
}
实例十一
也是实例九的变形,将d2方法改为实例方法,但是锁对象是一个静态属性。
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoFiveThree {
static Object classLock = new Object();
public void d1() {
synchronized (classLock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public void d2() {
synchronized (classLock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoFiveThree demo1 = new LockDemoFiveThree();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo1.d2(), "t2").start();
}
}
结论:因为锁对象是同一个静态属性,咱们知道静态属性是不可变的,所以不管是实例方法还是静态方法中的锁区块,拿到的都是同一把锁,不同线程需要按照拿到锁的先后顺序执行。
实例十二
两个线程通过不同的实例对象调用方法
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoSix {
public static synchronized void d1() {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
public static synchronized void d2() {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockDemoSix demo1 = new LockDemoSix();
LockDemoSix demo2 = new LockDemoSix();
new Thread(() -> demo1.d1(), "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> demo2.d2(), "t2").start();
}
}
执行效果和实例九是没有区别的,因为静态类方法不需要通过实例对象调用的,即使通过静态对象调用效果也一样。
实例十三
和实例十二一致,将锁对象换为静态Object类,执行效果一致
package com.lc;
import java.util.concurrent.TimeUnit;
/**
* @author liuchao
* @date 2023/4/7
*/
public class LockDemoSixTwo {
static Object classLock = new Object();
public static void d1() {
synchronized (classLock) {
try {
//模拟线程需要执行1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("d1线程名称:" + Thread.currentThread().getName());
}
}
public static void d2() {
synchronized (classLock) {
System.out.println("d2线程名称:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
LockDemoSixTwo demo1 = new LockDemoSixTwo();
LockDemoSixTwo demo2 = new LockDemoSixTwo();
new Thread(() -> demo1.d1(), "t1").start();
new Thread(() -> demo2.d2(), "t2").start();
}
}
总结:演示代码非常简单,但是通过稍微的改动,执行的结果却不一致。相信通过这些实例大家明白为啥阿里要强制”能用无锁数据结构,就不要用锁;能锁区块,就 不要锁整个方法体;能用对象锁,就不要用类锁“。
无锁最好,有锁也要控制锁的范围。锁的范围大小:方法内区块锁<方法体锁<对象锁<类锁。
暂无评论内容