显式锁和隐式锁的异同
显式锁和隐式锁都是为了保证线程安全Java官方提出来的解决办法,它们的区别简而言之就是 是否能自动 开/关锁 ,能自动 开/关锁 的属于隐式锁,需要程序员操作进行开关锁的属于显示锁。先看一下显示锁与隐式锁的使用方法:
解决的方法 | 格式 | 描述 |
---|---|---|
同步代码块(关键字) | synchronized(锁对象){} | 隐式锁,多个线程的锁对象必须唯一 |
同步方法(修饰符) | synchronized 返回类型 方法名(){} | 隐式锁,谁调用该方法谁就是锁对象 |
显示锁 | ReentrantLock类的lock()/unlock()方法 | 显式锁,有程序员决定在那开启/关闭锁 |
1.隐式锁
隐式锁分为同步代码块 和 同步方法两种实现方式,两者都是创建锁后自动开关锁,且使用的都是synchronized关键字,下面看一看具体的实现方式。
1.1同步代码块
以代码块为单位进行加锁。保证在执行一个线程的时候其他线程不能插足,使用排队执行原理的加锁机制。但是解决线程不安全问题以后效率变得不高了。
/**
* 实现方式
* 参数o : 被标记的锁对象,Java中任何对象都可以传进来,任何对象都可以打标记
*
* synchronized (o){}
*/
锁标记的对象,必须是一个任务的多个线程的锁 是一样的才能实现排队等待执行。使用实例:
public class Demo6 {
public static void main(String[] args) {
Runnable r = new Ticket();
//创建两个线程执行售票任务
new Thread(r).start();
new Thread(r).start();
}
//实现Runnable接口
static class Ticket implements Runnable{
private int count = 10;
//创建一个用于标记的锁对象
private Object o = new Object();
@Override
public void run() {//重写run任务 - 售票任务
while (true) {
synchronized (o) {//synchronized关键字 传入唯一的锁对象
if (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() +
"出票成功,余票" + count);
}else {
break;
}
}
}
}
}
}
上面提到的特别需要注意的一点就是:一个任务的所有线程只有都以一个锁对象作为判断依据才能解决线程不安全问题,倘若不是以一个属性来进行判断的,就无法解决线程不安全问题:
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
//锁对象在run()方法里面进行创建
//标记的锁,每创建一个线程就会创建一个锁,一个任务的多个锁不一样
Object o = new Object();
while (true) {
synchronized (o) {}
}
}
}
只有一个任务的所有线程都看同一把锁才能完成排队操作。
1.2同步方法
以方法为单位进行加锁。在方法名称前面加上修饰符synchronized,它的锁是this,调用该方法的对象就是锁,但当方法被静态修饰的时候锁对象就是类名.class。而创建(new)了多个对象以后,每个线程执行的对象互不相同,就不会由排队的情况,因为它们的锁不一样,给判断各自的锁。
public class Demo7 {
public static void main(String[] args) {
Runnable r = new Ticket();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {//重写run任务
while (true) {
boolean falg = sale();
if(!falg)
break;
}
}
public synchronized boolean sale(){//加上synchronized关键字锁方法 实现排队
if(count > 0){
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票"+count);
return true;
}//return的是false的话就结束卖票
return false;
}
}
}
输出的结果是
Thread-0出票成功,余票9
Thread-0出票成功,余票8
Thread-0出票成功,余票7
Thread-1出票成功,余票6
Thread-1出票成功,余票5
Thread-1出票成功,余票4
Thread-0出票成功,余票3
Thread-2出票成功,余票2
Thread-2出票成功,余票1
Thread-2出票成功,余票0
1.3需要注意的一点
当使用synchronized关键字来实现线程安全问题的时候,无论是使用同步代码块还是同步方法来实现线程安全,所有看一把锁的操作,都得排队。
2.显示锁
显示锁使用的是Lock类下的一个子类ReentrantLock类,原理就是由程序员创建锁/关闭锁。显式锁更能体现程序员在 操作/控制 锁的过程。
2.1ReentrantLock
ReentrantLock类的构造器
构造器 | 描述 |
---|---|
ReentrantLock() | 创建一个ReentrantLock的实例 |
ReentrantLock(boolean fair) | 使用给定的公平策略创建ReentrantLock实例 |
ReentrantLock类的创建锁与关闭锁的方法
变量和类型 | 方法 | 描述 |
---|---|---|
void | lock() | 获得锁 |
void | unlock() | 尝试释放此锁 |
下面就 实现Runnable接口创建了一个Ticket类。使得每次创建线程的操作 都是一个锁对象,使用的是同一把锁。每个线程在执行到它的时候它会检查锁对象是否被锁住,如果是锁住的就会排队等待,直到它不是锁的状态。
public class Demo8 {
public static void main(String[] args) {
Runnable r = new Ticket();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
private int count = 10;
//创建一个锁对象lock
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//锁住if()语句
//调用lock()方法
lock.lock();//被锁住以后其他线程就得进行等待
if(count > 0){
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票"+count);
}else break;
//if()使用完毕,解锁
//调用unlock()方法
lock.unlock();
}
}
}
}
3.关于抢先执行的问题
- 无论是显式锁还是隐式锁,当被锁住的代码块执行结束以后,正在等待的线程 开始抢时间偏执行代码块,并不是先来后到的方式进行执行,而是随机的的抢到时间偏就执行。
- 有意思的一点是 被锁住的代码块,谁先抢到时间偏进行代码执行以后,再次抢到的概率会增加,连续抢到的概率很大。