2Lock锁 (重点)
Posted zxhbk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2Lock锁 (重点)相关的知识,希望对你有一定的参考价值。
Lock锁
传统 synchronized
真正的多线程开发,公司中的开发,需要降低耦合度
线程是一个单独的资源,没有任何附属的操作!
单独的资源包含属性、方法
第一种:高耦合写法,Ticket
线程类还有附属操作,不推荐使用
public class SaleTicketDemo01 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(ticket).start(); } } class Ticket implements Runnable{ @Override public void run() { } }
第二种:降低耦合度
//举例买票的栗子 /** * 真正的多线程开发,公司中的开发,降低耦合性 * 线程就是一个单独的资源类,没有任何附属的操作! * 1、 属性、方法 */ public class SaleTicketDemo01 { public static void main(String[] args) { // 并发,就是多个线程操作同一份资源,使用时直接丢入线程 Ticket ticket = new Ticket(); // 3个人同时卖票 // Runnable接口标注着 @FunctionalInterface 表示函数式接口,在jdk1.8,可以使用lambda表达式 ()->{} new Thread(() -> { for (int i = 0; i < 40; i++) { //卖40张肯定会卖完 ticket.sale(); //调用买票方法,就是操作资源 } }).start(); new Thread(() -> { for (int i = 0; i < 40; i++) { ticket.sale(); } }).start(); new Thread(() -> { for (int i = 0; i < 40; i++) { ticket.sale(); } }).start(); } } class Ticket{ //剩余的票 private int number = 30; // 售票方法 // synchronized同步方法,本质:相当于队列,锁 // 比如:食堂学生打饭,不排队学生就会一拥而至;排队的情况下,相当于一个人打饭会有一个锁,打完饭后释放锁; public synchronized void sale(){ if(number > 0){ System.out.println("已售出第" + number-- + "张票,剩余:" + number + "张"); } } }
多线程下产生并发问题,需要加上synchronized,同步方法;
Lock接口
解析
1)实现类
2)使用方法
3) 可重入锁:ReentrantLock类
有两种构造方法,可以构造非公平锁和公平锁,默认是公平锁!
公平锁:顾名思义非常的公平
- 讲究一个先来后到
- 比如:两个线程的执行时间分别是 3s 和 3h,那么 3h 在 3s 前面,那么必须等待 3h 之后才能执行!
非公平锁:顾名思义它是不公平的
- 可以被插队!(默认的)
买票的栗子
package com.zxh.demo01; //举例买票的栗子 import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SaleTicketDemo02 { public static void main(String[] args) { // 并发,就是多个线程操作同一份资源,使用时直接丢入线程 Ticket ticket = new Ticket(); // 3个人同时卖票 new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "B").start(); new Thread(() -> { for (int i = 0; i < 40; i++) ticket.sale(); }, "C").start(); } } // Lock三部曲 // 1、new ReentrantLock(); 创建锁 // 2、lock.lock(); // 加锁 // 3、lock.unlock(); // 解锁 class Ticket2 { //剩余的票 private int number = 30; Lock lock = new ReentrantLock(); // 售票方法 public void sale(){ lock.lock(); // 加锁 // lock.tryLock(); //尝试获取锁,只有在调用时它不被另一个线程占用才能获取锁,获取成功返回true,否则返回false try { // 业务代码 if(number > 0){ System.out.println(Thread.currentThread().getName() + "已售出第" + number-- + "张票,剩余:" + number + "张"); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); // 解锁 } } }
问题:什么是可重入锁呢?
广义上的可重入锁:
可重入锁就是可以重复可递归调用的锁,就是在外层使用锁后,内层依旧可以使用该锁,并且不会发生死锁(可重入锁:前提锁的是同一个对象或者class,如果锁的东西不同,那就不是可重入锁了)。
可重入锁是以线程为单位的,比如:当一个线程获取对象加锁后,该线程可以再次获取该对象的锁,但是其他线程不行,需要等待该线程释放锁。
synchronized
和 ReentrantLock
都是可重入锁。
解释了这么多,可能还是不太明白,接下来通过栗子进行解释什么是可重入锁。
可重入锁:synchronized
public class MyTest { public static void main(String[] args) { Data data = new Data(); /* synchronized:锁的是方法的调用者,就是对象 data 现在创建两条线程,各自去调用10次get方法,或者更多 你会发现,一个线程获取了两次锁,并没有发生死锁,并且哪个线程谁先拿到锁,其他的线程只能等待 (运行结果可能存在,B线程在A线程之间调用了get和set方法, 那是因为A线程释放了锁,并且CPU刚好去执行B线程了,尽管如此你还是会发现,get()set()方法都是连在一起执行的) 所以:这就是可重入锁,该锁可以被重复递归的调用 */ // 这里为了截图方便调用3次get()方法 new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start(); } } class Data{ /** * 两个方法都是同步方法,作用:打印线程的名字 * get()方法调用set(),一个同步方法调用另一个同步方法 */ public synchronized void get(){ System.out.println(Thread.currentThread().getName() + "=> get()"); set(); } public synchronized void set(){ System.out.println(Thread.currentThread().getName() + "=> set()"); } }
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MyTest { public static void main(String[] args) { Data data = new Data(); // 这里为了截图方便调用3次get()方法 new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start(); } } class Data{ Lock lock = new ReentrantLock(); // 可重入锁 /** * 作用:打印线程的名字 * get()方法调用set(),一个同步方法调用另一个同步方法 */ public void get(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> get()"); set(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void set(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> set()"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
自定义不可重入锁
-
现在我们自己定义一个简单的锁,并且不是可重入锁,我们可以看一下会发生什么
public class MyTest { public static void main(String[] args) { Data data = new Data(); // 这里为了截图方便调用3次get()方法 new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start(); // new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start(); } } class Data{ MyLock lock = new MyLock(); // 可重入锁 /** * 作用:打印线程的名字 * get()方法调用set(),一个同步方法调用另一个同步方法 */ public void get(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> get()"); set(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void set(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> set()"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } // 自定义简单锁 class MyLock{ private boolean flag = false; // 使用变量控制该锁是否被占用 // 加锁 public synchronized void lock() { while (flag){ // 如果是true,那么表示该锁被占用 try { this.wait(); // 线程等待 } catch (InterruptedException e) { e.printStackTrace(); } } flag = true; // 加锁 } // 解锁 public synchronized void unlock(){ flag = false; // 释放锁 notifyAll(); // 唤醒其他线程 } }
接下来说一下可重入锁的实现原理?
实现原理:通过为每一个锁关联一个请求计数器和一个占用它的线程。当计数器为0,代表该锁没有被占用;当线程请求一个未被占用的锁,那么JVM将记录锁的占用者,并且将请求计数器置为1。
如果同一个线程再次请求该锁,那么计数器会递增+1。
每次占用线程退出同步块,请求计数器会 -1,直到计数器为0才释放锁。
修改自定义的锁为可重入锁
public class MyTest { public static void main(String[] args) { Data data = new Data(); // 这里为了截图方便调用3次get()方法 new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 3; i++) data.get(); }, "B").start(); } } class Data{ MyLock lock = new MyLock(); // 可重入锁 /** * 作用:打印线程的名字 * get()方法调用set(),一个同步方法调用另一个同步方法 */ public void get(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> get()"); set(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void set(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> set()"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } // 自定义简单锁 class MyLock{ private boolean flag = false; // 使用变量控制该锁是否被占用 private int lockCount = 0; // 关联一个请求计数器 private Thread thread = null; // 关联一个占用它的线程 // 加锁 public synchronized void lock() { Thread currentThread = Thread.currentThread(); while (flag && this.thread != currentThread){ // 如果该锁被占用,并且进入的线程不是当前占用的线程 try { this.wait(); // 线程等待 } catch (InterruptedException e) { e.printStackTrace(); } } // 锁没有被占用 或者 锁被占用但是为同一个线程进入 flag = true; // 加锁 lockCount++; // 请求计数器+1 this.thread = currentThread; // 被当前进入的线程占用 } // 解锁 public synchronized void unlock(){ if (this.thread == Thread.currentThread()){ // 如果进入的线程是当前占用的线程 lockCount--; // 请求计数器 -1 if (lockCount == 0){ // 如果请求计数器为0 flag = false; // 释放锁 notifyAll(); // 唤醒其他线程 } } } }
synchronized 和 Lock 区别
特征区别
- synchronized:是Java的关键字,Lock:是Java的类
- synchronized:适合锁少量的同步代码,而Lock:适合锁大量的同步代码
详细区别
- synchronized:会自动释放锁,Lock:需要手动释放锁。
- synchronized:无法判断锁的状态,Lock:可以判断锁的状态(锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁,会根据线程之间的竞争从前到后转换)
- synchronized:多个线程会等待(比如:线程1(阻塞),线程2(死死等待)),Lock:就不一定会等待,可以通过 lock.tryLock() 尝试获取锁。
- synchronized:是可重入锁,不可以中断,非公平锁,Lock:也是可重入锁,自由度高,可以判断锁,非公平锁(可以手动设置为公平锁)。
以上是关于2Lock锁 (重点)的主要内容,如果未能解决你的问题,请参考以下文章
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段