悲观锁
乐观锁
Lock8 8锁问题
①. 标准访问有ab两个线程,请问先打印邮件还是短信
②. sendEmail方法暂停3秒钟,请问先打印邮件还是短信
③. 新增一个普通的hello方法,请问先打印邮件还是hello
④. 有两部手机,请问先打印邮件还是短信
⑤. 两个静态同步方法,同1部手机,请问先打印邮件还是短信
⑥. 两个静态同步方法, 2部手机,请问先打印邮件还是短信
⑦. 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
⑧. 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
结论:用的是不是同意一把锁 ,锁对象还是锁类
class Phone{ //资源类
public static synchronized void sendEmail() {
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-------sendEmail");
}
public synchronized void sendSMS()
{
System.out.println("-------sendSMS");
}
public void hello()
{
System.out.println("-------hello");
}
}
public class Lock8Demo{
public static void main(String[] args){//一切程序的入口,主线程
Phone phone = new Phone();//资源类1
Phone phone2 = new Phone();//资源类2
new Thread(() -> {
phone.sendEmail();
},"a").start();
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start();
}
}
/**
*
* ============================================
* 1-2
* * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
*
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* 7-8
* 当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
* *
* * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* *
* * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
**/
synchronized与Lock的
相同:
实现类都是悲观锁
区别:
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
公平锁和非公平锁
①. 什么是公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到
非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象
注意:synchronized 和 ReentrantLock 默认是非公平锁
②. 排队抢票案例(公平出现锁饥饿)
锁饥饿:我们使用5个线程买100张票,使用ReentrantLock默认是非公平锁,获取到的结果可能都是A线程在出售这100张票,会导致B、C、D、E线程发生锁饥饿(使用公平锁会有什么问题)
class Ticket {
private int number = 50;
//true 设置公平锁,慢慢变公平
private Lock lock = new ReentrantLock(true); //默认用的是非公平锁,分配的平均一点,=--》公平一点
public void sale() {
lock.lock();
try {
if(number > 0) {
System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
}
}finally {
lock.unlock();
}
}
/*Object objectLock = new Object();
public void sale(){
synchronized (objectLock)
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"\t 卖出第: "+(number--)+"\t 还剩下: "+number);
}
}
}*/
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"c").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"d").start();
new Thread(() -> { for (int i = 1; i <=55; i++) ticket.sale(); },"e").start();
}
}
④. 为什么会有公平锁、非公平锁的设计?为什么默认非公平?面试题
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的
角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU
空闲状态时间
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,
然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状
态的概率就变得非常大了,所以就减少了线程的开销线程的开销
⑤. 什么时候用公平?什么时候用非公平?面试题
(如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就
上去了。否则那就用公平锁,大家公平使用)
可重入锁(又名递归锁)
①. 什么是可重入锁?
可重入锁又名递归锁,是指在 同一个线程 在外层方法获取锁的时候,再进入该线程的 内层方法 会自动获取锁(前提,锁对象得是同一个对象), 不会因为之前已经获取过还没有释放而阻塞
如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
所以Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
②. 可重入锁这四个字分开解释
可: 可以 | 重: 再次 | 入: 进入 | 锁: 同步锁 | 进入什么:进入同步域(即同步代码块、方法或显示锁锁定的代码)
③. 代码验证synchronized和ReentrantLock是可重入锁
同步代码块
public class ReEntryLockDemo{
public static void main(String[] args){
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA){
System.out.println("-----外层调用");
synchronized (objectLockA){
System.out.println("-----中层调用");
synchronized (objectLockA){
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
同步方法
public class ReEntryLockDemo{
public synchronized void m1(){
System.out.println("-----m1");
m2();
}
public synchronized void m2(){
System.out.println("-----m2");
m3();
}
public synchronized void m3(){
System.out.println("-----m3");
}
public static void main(String[] args){
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
2、显式锁(即Lock)也有ReentrantLock这样的可重入锁。
public class Demo4 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo4.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo4.num);
}
}
上面代码中add()方法中,当一个线程进入的时候,会执行2次获取锁的操作,运行程序可以正常结束,并输出和期望值一样的30000,假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一样,可重入锁。
代码中还有几点需要注意:
lock()方法 和 unlock()方法需要 成对出现,锁了几次,也要释放几次,否则后面的线程无法获取
锁了;可以将add中 的 unlock删除一个事实,上面代码运行将无法结束
unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
public class ReEntryLockDemo{
static Lock lock = new ReentrantLock();
public static void main(String[] args){
new Thread(() -> {
lock.lock();
try
{
System.out.println("----外层调用lock");
lock.lock();
try
{
System.out.println("----内层调用lock");
}finally {
// 这里故意注释,实现加锁次数和释放次数不一样
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock(); // 正常情况,加锁几次就要解锁几次
}
}finally {
lock.unlock();
}
},"a").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("b thread----外层调用lock");
}finally {
lock.unlock();
}
},"b").start();
}
}