引入
了解显式锁与隐式锁之前,先来看一个日常生活中卖票的例子:
- 首先新建类Ticket 实现接口Runnable,新建run卖票任务
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(count > 0){
//卖票
System.out.println("正在准备卖票");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println("出票成功!余票:" + count);
}
}//end
}
- 在main中创建并启动三个线程
public static void main(String[] args) {
//线程不安全
Runnable runnable = new Ticket();
//启动三个线程
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
输出结果:
正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1
这是一个的卖票栗子,总共有10张票,有3个线程分别进行卖票。通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了
出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同
这就是多线程完成统一任务(一个任务交给三个线程去执行)时出现的线程不安全问题
为了解决线程不安全问题,我们在写代码的时候常常用到锁来保证线程的安全
显式锁与隐式锁
所谓的显式和隐式,就是在使用的时候使用者是否需要手动写代码去获取锁和释放锁
显式锁 | 隐式锁 |
---|---|
需要手动写代码去获取锁和释放锁 | 不需要手动写代码去获取锁和释放锁 |
显式锁使用Lock关键字 | 隐式锁使用synchronized修饰符 |
一、 隐式锁
隐式锁使用synchronized修饰符
在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁,不需要手动写代码去获取锁和释放锁
1.1 同步代码块
线程同步,使线程排队执行
每个线程在执行时看同一把锁,谁抢到了锁,谁就执行
线程同步实现:synchronized
格式:
synchronized(锁对象){
// 同步代码块
}
`
锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
代码示例:
对原有的线程不安全的卖票示例进行修改
在while循环中加锁
同步代码块为:当余票大于0时,进行卖票操作
因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行
/**
* 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
*
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();//创建对象
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
}
由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁
而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,谁抢到锁谁就执行,排队执行
Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个
//启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个o
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
//如果上述写法写成如下,则依旧为不安全线程
//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张
//错误写法!!!!注意
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
//new Thread(new Ticket()).start();
加了锁之后的输出结果:
正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0
如果将创建锁的对象写在任务的代码块中,如下所示
此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!
错误写法:
public void run() {//每次被触发就进卖买票操作
Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队
while(true){
synchronized (o){//加锁
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
}
}//end while
}//end run
1.2 同步方法
与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符
同步方法的锁为this
同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class
代码示例:
/**
* 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题
* 解决线程不安全问题:排队执行
*/
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
boolean flag = sale();//sale()为加了锁的方法
if(!flag){
break;
}
}//end while
}//end run
//添加synchronized修饰符,给方法加锁
public synchronized boolean sale(){
//this,同步的方法的锁
//Ticket.class,如果方法为静态方法,则同步方法的锁为类.class
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
return true;
}
return false;
}
}
如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行
如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行
public void run() {
synchronized (this){//再加一把锁
}
while(true){
boolean flag = sale();
if(!flag){
break;
}
}//end while
}//end run
如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行
二、 显式锁
显式锁使用Lock关键字
在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制
显式锁Lock的子类:ReentrantLock
代码示例:
- 创建隐式锁l
Lock l = new ReentrantLock();
- 在进行代码块前锁住
l.lock();
- 在代码块结束后开锁
l.unlock();//代码执行完毕,开锁
完整代码如下:
public static void main(String[] args) {
//线程不安全
//解决方案3:显式锁Lock
//java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
Runnable runnable = new Ticket();//只有一个任务
//启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁l
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//创建显式锁l
private Lock l = new ReentrantLock();
@Override
public void run() {//每次被触发就进卖买票操作
while(true){
l.lock();//进入if之前,锁住
if(count > 0){
//卖票
System.out.println("正在准备卖票,请稍等...");
//try-catch使得卖票的时间更长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count --;
System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
}else{
break;
}
l.unlock();//代码执行完毕,开锁
}//end while
}//end run
}
不论是显式锁还是隐式锁,都可以有效地控制多线程获取资源、解决所出现的线程不安全问题
公平锁与非公平锁
- 公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁
- 非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁
实现公平锁:
显式锁Lock的构造方法中,参数为True则表示公平锁
Lock l = new ReentrantLock(true);