目录
三、读写锁 (ReentrantReadWriteLock)
一、JUC概述及回顾
在 Java 5.0 提供了 java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。
Int i = 0, i ++; 非原子性,分三步;需要用到atomic 原子性容器
线程和进程
每一个程序都有一个进程,操作系统动态执行的基本单元;
一个进程中可以包含若干个线程;线程是资源调度的最小单位
常识:一个进程中通常有3000 - 5000个线程
生活实例:
使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,这就是线程。QQ支持录入信息的搜索。
大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查
Nginx 多个进程
并行和并发
并行:同一时间多个线程在执行,同一时刻多个线程在访问同一个资源, (烧水泡面)
并发:同一时间多个线程在做同一件事 (秒杀、春运抢票)
wait/sleep的区别
wait 释放锁,sleep 不释放锁
wait是Object的方法,sleep是Thread
wait 一般搭配 notify 使用
相同点:在哪睡的在哪醒
创建线程的方式:
继承Thread抽象类;
实现Runnable接口:
实现JUC 的Callable接口
实现线程池 (常用)
lambda表达式
Lambda 是一个匿名函数,使代码更简洁、更灵活
左侧:指定了 Lambda 表达式需要的所有参数
右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
复制小括号(参数),写死右箭头(->),落地大括号{}
前提:Lambda,省略了方法名。要求必须是函数式接口 @FunctionalInterface
//Foo foo = (int x,int y) -> {return x+y;}; Foo foo = (x,y) -> x+y; System.out.println(foo.add(10,20));
函数式接口扩展
1.8之后 接口可以实现default(缺省方法)方法、静态方法
@FunctionalInterface interface Foo{ public int add(int x,int y); default int sub(int x,int y){ return x - y; } public static int div(int x,int y){ return x/y; } }
synchronized的8锁问题
多线程编程模板上:高内聚,低耦合;线程 操作 资源类
// 资源类 class Phone{ // 发送短信 public synchronized void sendMsg() throws InterruptedException { System.out.println("----发送短信方法...."); } // 发送邮件 public synchronized void sendEmail(){ System.out.println("----发送邮件方法...."); } // 打电话 public void getHello(){ System.out.println("----打电话 hello word!...."); } } //1. 标准访问,先打印短信还是邮件 //2. 停4秒在短信方法内,先打印短信还是邮件 //3. 普通的hello方法,是先打短信还是hello //4. 现在有两部手机,先打印短信还是邮件 //5. 两个静态同步方法,1部手机,先打印短信还是邮件 //6. 两个静态同步方法,2部手机,先打印短信还是邮件 //7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件 //8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件 public class Lock_8 { public static void main(String[] args) { // 线程操作资源类 Phone phone = new Phone(); // Phone phone2 = new Phone(); // 定义一个线程: new Thread(()->{ try { phone.sendMsg(); } catch (InterruptedException e) { e.printStackTrace(); } },"AA").start(); // 定义一个线程: new Thread(()->{ phone.sendEmail(); // phone2.sendEmail(); // phone.getHello(); },"BB").start(); } } *答案 //1. 标准访问,先打印短信还是邮件(短信) //2. 停4秒在短信方法内,先打印短信还是邮件(短信) //3. 普通的hello方法,是先打短信还是hello(hello) //4. 现在有两部手机,先打印短信还是邮件(邮件) //5. 两个静态同步方法,1部手机,先打印短信还是邮件(短信) //6. 两个静态同步方法,2部手机,先打印短信还是邮件(短信) //7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件(邮件) //8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件(邮件)
普通同步方法,锁是当前实例对象 this。
静态同步方法,锁是当前类的Class对象。
同步方法块,锁是Synchonized括号里配置的对象
所有的静态同步方法用的是同一把锁—— 类对象本身。
项目中用synchronized还是Lock?项目中其他模块用的什么锁,保持一致。先去模仿再去创新
二、Lock 锁 ★★★
Lock 实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 (公平锁,非公平锁,共享锁,独占锁...)
Lock 是一个接口,这里主要有三个实现:ReentrantLock(可重复的)、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock (可重复的读写锁)
参考API:
注意:synchronized和Lock不能同时使用
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // 上锁
try {
// ... method body
} finally {
lock.unlock() // 解锁
}
}
}
改造后的售票方法
//资源类
class Ticket{
private Integer number = 20;
private Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); //上锁
try {
//判断有没有票
if(number<=0){
System.out.println("票已售空");
return;
}
Thread.sleep(200);
number--;
System.out.println(Thread.currentThread().getName()+"卖票成功,剩余票数"+number);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
}
可重入锁 (递归锁) ReentrantLock
可重入锁又名递归锁(锁可以传递),是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。Java中ReentrantLock 和 synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
class A{
public synchronized void aa{
......
bb();
......
}
public synchronized void bb{
......
}
}
A a = new A();
a.aa();
ReentrantLock和synchronized区别
二者都是独占锁
(1) synchronized加锁和解锁的过程自动进行,ReentrantLock 上锁解锁需要手动进行,比较灵活,
(2) synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3) synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
公平锁
所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
在创建ReentrantLock锁时,加入Boolean参数,默认非公平
注意:底层多维护了一个队列,效率会降低
private Lock lock = new ReentrantLock(true);
ReentrantLock() 创建一个 ReentrantLock 的实例。 |
ReentrantLock(boolean fair) 创建一个具有给定公平策略的 ReentrantLock(公平锁) 。 |
限时等待上锁 tryLock (解决死锁)
tryLock 方法可以选择传入指定的等待时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。
boolean | tryLock() 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。 |
boolean | tryLock(long timeout, TimeUnit unit) 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。 |
boolean flag = false;
try {
//限时等待上锁
flag = lock.tryLock(3,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
三、读写锁 (ReentrantReadWriteLock)
读写锁允许同一时刻被多个读线程访问,写线程访问时,读线程和其他的写线程都会被阻塞。
同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;
生活案例:开会投影仪、直播、红蜘蛛
- 写写不可并发
- 读写不可并发 (脏读)
- 读读可以并发
避免死锁:锁的轻一点,范围小一些
1. 创建实例 没有锁的情况
class MyCache{
// volatile : 在多线程中, 被它修饰的变量是可见的!(防止指令重排)
private volatile Map<String,String> map = new HashMap<>();
//写数据
public void pot(String key,String value){
try {
System.out.println(Thread.currentThread().getName()+"开始写入-----");
Thread.sleep(200);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入完成-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//写数据
public void get(String key){
try {
System.out.println(Thread.currentThread().getName()+"开始读取-----");
Thread.sleep(200);
String msg = map.get(key);
System.out.println(Thread.currentThread().getName()+"---读取完成......."+msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//创建5个写线程
for (int i = 0; i < 5; i++) {
String num = String.valueOf(i);
new Thread(()->{
myCache.pot(num,num);
},num).start();
}
//创建5个写线程
for (int i = 0; i < 5; i++) {
String num = String.valueOf(i);
new Thread(()->{
myCache.get(num);
},num).start();
}
}
}
顺序被打乱,脏读、并发修改异常!
2. 参考API
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);
rwl.readLock().unlock();
}
}
3. 加入读写锁
class MyCache{
// volatile : 在多线程中, 被它修饰的变量是可见的!(防止指令重排)
private volatile Map<String,String> map = new HashMap<>();
//读写锁
private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
//写数据
public void pot(String key,String value){
rwlock.writeLock().lock(); //上写锁 (排他锁)
try {
System.out.println(Thread.currentThread().getName()+"开始写入-----");
Thread.sleep(200);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入完成-----");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwlock.writeLock().unlock(); //解开写锁
}
}
//写数据
public void get(String key){
rwlock.readLock().lock(); //读写锁 (共享锁)
try {
System.out.println(Thread.currentThread().getName()+"开始读取-----");
Thread.sleep(200);
String msg = map.get(key);
System.out.println(Thread.currentThread().getName()+"---读取完成......."+msg);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwlock.readLock().unlock(); //解开写锁
}
}
}
锁降级 (了解)
独占改为共享可以(降级),共享改为独占不可(升级)。
什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:
// 锁的降级:从写锁降级成为读锁,随后释放写锁的过程就是锁降级!
public void c(){
rwl.writeLock().lock();
System.out.println("写锁--------------");
//写锁还没有释放之前,获取读锁
rwl.readLock().lock();
System.out.println("读锁===============");
rwl.writeLock().unlock();
System.out.println("释放写锁+++++++++++");
rwl.readLock().unlock();
System.out.println("释放读锁###########");
}
读写锁总结
1. 支持公平/非公平策略
2. 支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁3. 支持锁降级,不支持锁升级
4. 读写锁如果使用不当,很容易产生“饥饿”问题:
- 在读线程非常多,写线程很少的情况下,容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但“公平”策略会使系统吞吐量降低。 (底层排队,维护队列效率降低)5. Condition 条件支持 (钥匙)
- 写锁可以通过newCondition()
方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()
方法会直接抛出UnsupportedOperationException
。