Java提供了两种锁机制来控制多个线程对共享资源的互斥访问
Synchronized
:同步关键字,属于Jvm内置关键字,由虚拟机控制ReentrantLock
:可重入锁,JDK实现,由开发人员控制当多个线程对同一资源进行访问时,可以通过Synchronized关键字去进行加锁,以防止线程安全问题。JVM将加锁的技术包装成关键字,降低门槛,非常容易使用。
Synchronized的作用对象:
使用this表示当前对象
public void Demo{
public void func(){
synchronized(this){
// ...
}
}
}
举个例子,使用synchronized锁住当前对象
public void Demo{
public void func(){
synchronized(this){
for(int i=0;i<10;i++){
System.out.print(i + " ");
}
}
}
}
同一个对象,开启两个线程执行
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
Demo demo = new Demo();
es.execute(()->demo.func());
es.execute(()->demo.func());
executorService.shutdown();
// 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}
不同对象,各自开启线程执行
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
Demo demo1 = new Demo();
Demo demo2 = new Demo();
es.execute(()->demo1.func());
es.execute(()->demo2.func());
executorService.shutdown();
// 输出 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
}
因此,如果需要将代码块的锁上升到整个类的话,可以使用下面的方式
指定Demo.class,表示作用于整个类
public void Demo{
public void func(){
synchronized(Demo.class){
// ...
}
}
}
举个例子,使用synchronized锁住当前对象
public void Demo{
public void func(){
synchronized(Demo.class){
for(int i=0;i<10;i++){
System.out.print(i + " ");
}
}
}
}
不同对象,各自开启线程执行
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
Demo demo1 = new Demo();
Demo demo2 = new Demo();
es.execute(()->demo1.func());
es.execute(()->demo2.func());
executorService.shutdown();
// 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}
同代码块加锁的作用域一致,synchronized作用于方法时,也可以对当前对象,或者当前类进行加锁。
public void Demo{
public synchronized void func(){
// ...
}
}
public void Demo{
public static synchronized void func(){
// ...
}
}
是的,你没有看错,只要将方法变成静态的,就可以将作用域上升到类。
talk is cheap, show you my code ~我们用一段代码来直观地看下可重入锁的基本使用
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 加锁。当线程拥有锁权限时,可以往下执行;否则在此阻塞
lock.unlock(); // 解锁。
再上一段实战代码,了解一下真实场景的使用方式
public class Demo{
private static ReentrantLock lock = new ReentrantLock();
public void func(){
try {
// 加锁
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
Demo demo1 = new Demo();
Demo demo2 = new Demo();
es.execute(()->demo1.func());
es.execute(()->demo2.func());
executorService.shutdown();
// 输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
}
ReentrantLock一般要与try-finally结合使用,主要释放锁(避免造成死锁)
对比指标 | Synchronized | ReentrantLock | 理由 | 使用场景 |
---|---|---|---|---|
风险/使用难度 | 胜 | 负 | Synchronized由jvm控制,开发人员只需要进行加锁即可,锁的释放由jvm管理,不用担心死锁情况;而ReentrantLock需要开发人员自行加锁,自行解锁,从风险角度考虑,Synchronized更胜一筹 | 简单场景 |
顺序 | 负 | 胜 | Synchronized是非公平锁,无法保证线程执行的顺序;而ReentrantLock可以通过构造器ReentrantLock(boolean fair)去初始化一个公平锁 | 对线程池队列顺序有要求时使用 |
灵活性 | 负 | 胜 | Synchronized由jvm控制,因此开发人员无法自行解锁;ReentrantLock可以自信定义加锁解锁,更加灵活 | 复杂场景,在程序中需要手动解锁 |
性能 | 平 | 平 | 参考网上说吧,这个还没验证过。但是性能不是凭证,这个指标不作为参考依据 | 无 |
此外,jvm对Synchronized还做了非常多的锁优化策略,比如偏向锁,自旋锁,轻量级锁,重量级锁,这个以后再专门来讲。
学习完两种加锁方式后,我们应当清楚业务场景是啥,再决定使用哪种锁去实现。简单锁使用Synchronized,复杂的使用ReentrantLock。