0 导学
1 JUC概述
JUC概念
处理线程的工具包 java.util .concurrent 的简称
Java8API
进程、线程概念
1.1 进程 & 线程
进程 | 线程 |
---|---|
系统中正在运行的一个应用程序 / 运行的程序 | 系统分配处理器时间资源的基本单元 / 进程内独立执行的一个单元执行流 |
资源分配的最小单位 | 资源调度的最小单位 程序使用CPU的最基本单位 |
一个进程可并发多个线程 | 每条线程并行执行不同的任务 |
1.2 线程状态
Thread.State 线程状态枚举类
线程状态转换图
参考
1.3 sleep & wait
异 | sleep | wait |
---|---|---|
所属类 | Thread静态方法 | Object方法 任何实例对象都可调用 |
时间 | 指定时间 | 可指定(限时等待)可不指定(无限等待) |
释放锁 | 释放CPU执行权 不释放同步锁 | 释放CPU执行权、同步锁 |
使用的地方 | 任何地方都能使用 | 只能在同步代码方法/块中使用(调用前提:当前线程占有锁->代码要在 synchronized 中) |
捕获异常 | 必须捕获异常 | 捕获/抛出异常 |
同:可被 interrupted 方法中断;在哪里睡,就在哪里醒 |
1.4 并发 & 并行
串行:所有任务按先后顺序执行
并行:同一时刻多个线程在访问同一个资源
并发(concurrent):多项工作一起执行,之后再汇总
1.5 管程
管程-Monitor监视器(OS) 锁(Java):是一种同步机制,保证同一时间,只有一个线程访问被保护数据/代码
JVM同步基于进入(加锁)和退出(解锁),使用管程对象实现(对临界区加锁和解锁)
1.6 用户线程 & 守护线程
用户线程:自定义线程
守护线程:如垃圾回收(后台执行)
2 Lock接口
2.1 synchronized、Lock 对比、介绍
异 | synchronized | Lock |
---|---|---|
存在层次 | Java关键字 内置特性 托管给ivm执行 | 接口 非Java内置 是Java写的控制锁的代码 |
释放锁 | 异常->自动unlock(JVM会让线程释放锁) | 必须在finally中手动unlock,否则死锁 |
获取锁 | 不可响应中断,会一直等待锁释放 | 等锁线程可响应中断(interrupt) |
锁状态 | 无法判断 | 可用trylock()判断是否成功获取锁 |
锁类型 | 可重入 非公平 | 可重入 非公平(默认)/公平(传参true) |
性能 | 竞争资源不激烈,二者差不多(适合少量同步) | 竞争资源激烈,性能优(提高多线程读效率) |
优点 | 使用简单(JDK1.6后优化:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁) | 灵活 |
底层 | CPU悲观锁机制-线程获得的是独占锁 | 乐观锁 -CAS(Compare and Swap)实现 |
调度机制 | Object wait() notifyAll() notify()-this.notify(),JVM随机唤醒某个等待的线程 | Condition await() signalAll() signal()-ci.signal(),选择性通知ci |
synchronized隐式(内置)锁 & Lock显式锁 |
- synchronized好用,简单,性能不差
- 没有使用到Lock显式锁的特性就不要使用Lock锁了
Lock.java
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock同步形式
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock();
// block until condition holds
try {
// ... method body
} finally {
lock.unlock(); //Lock发生异常时,若不主动释放锁,会死锁->finally保证不管有无异常都会释放锁
}
}
}
2.2 eg.卖票
⭐多线程编程步骤(高内聚 低耦合)
- 创建资源类 定义属性、方法
- 创建多个线程 调用资源类方法
synchronized实现卖票
//1.创建资源类 定义属性、方法
class Ticket {
//票数
private int number=30;
//卖票
public synchronized void sale() { //synchronized自动上锁 去掉关键字 输出全是A卖出的
if(number>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+number--+"号票");
}
}
}
public class Test1_SaleTicket {
//2.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Ticket ticket=new Ticket();
//创建3个线程
new Thread(new Runnable() { //匿名内部类
@Override
public void run() {
//调用资源类方法
for(int i=0;i<40;i++) {
ticket.sale();
}
}
}, "A").start();
//同理创建线程B、C
}
}
Lock实现卖票
import java.util.concurrent.locks.ReentrantLock;
//1.创建资源类 定义属性、方法
class Ticket {
//票数
private int number=30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
//卖票
public void sale() { //用ReentrantLock手动上锁
//加锁
lock.lock();
try{
if(number>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+number--+"号票");
}
} finally {
//解锁
lock.unlock();
}
}
}
public class Test1_LSaleTicket {
//2.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Ticket ticket = new Ticket();
//创建3个线程
new Thread(() -> { //Lambda表达式
//调用资源类方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "A").start(); //线程调用start后可能马上创建(OS空闲),也可能等会儿创建(OS忙) 取决于OS
//同理创建线程B、C
}
}
线程执行顺序不固定的原因
3 线程间通信
线程间通信模型:共享内存、消息传递
⭐多线程编程步骤
- 创建资源类 定义属性、方法
- 在资源类操作方法(判断 干活 通知)
- 创建多个线程 调用资源类方法
- 防止虚假唤醒问题(判断条件要加到while中)
实现线程间交替+1-1操作
- 用synchronized关键字实现
this.wait()/notifyAll();
//1.创建资源类 定义属性、方法
class Share {
private int i=0;
//2.在资源类操作方法(在方法中 判断 干活 通知)
public synchronized void incr() throws InterruptedException {
//判断
while(i!=0) {
this.wait(); //不满足干活条件->在被通知前<等待> 释放锁
}
//干活
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知其它线程
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
while(i!=1) {
this.wait(); //if的wait:在哪里睡,在哪里醒 被调用时唤醒->虚假唤醒 解决:条件放到while中
}
//干活
i--;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知
this.notifyAll();
}
}
public class Test2_ThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Share share=new Share();
//创建2个线程 交替实现+1-1
//若是多个线程,等待条件放在if中:+的锁释放后又被+的线程抢到了,会+到>1 -的操作也会-到<0
new Thread(()->{
for(int i=0;i<10;i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//同理创建线程B、C、D
}
}
- 用Lock接口实现
new ReentrantLock().newCondition().await()/signalAll();
//1.创建资源类 定义属性、方法
class Share {
private int i=0;
//创建lock
private Lock lock=new ReentrantLock();
private Condition condition=lock.newCondition();
//2.在资源类操作方法
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(i!=0) {
condition.await();
}
//干活
i++;
System.out.println(Thread.currentThread().getName()+":"+i);
//通知
condition.signalAll();
} finally {
//解锁
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while(i!=1) {
condition.await();
}
i--;
System.out.println(Thread.currentThread().getName()+":"+i);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class Test2_LThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
Share share=new Share();
//创建多个线程
new Thread(()->{
for(int i=0;i<10;i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//同理创建线程B-decr、C-incr、D-decr
}
}
线程虚假唤醒
- 问题
- 原因
- 解决
- 分析
理解虚假唤醒问题:坐飞机(调用线程)要进行安检(判断条件),下飞机(线程睡眠)再上飞机(唤醒线程)还要进行安检(while醒来条件不符合继续睡),否则只安检一次(if)的话可能会有可疑物品带上飞机(程序会往下执行)
4 线程间定制化通信
- 用 notify()通知时,JVM会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知
- 在调用 Condition 的 await()/signal()方法前,也需要线程持有相关锁,调用 await()后线程会释放这个锁,在 singal() 调用后会从当前 Condition 对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦成功获得锁就继续执行
A 线程打印5次A,B线程打印10次B,C线程打印15次C,按照
此顺序循环10轮->用Lock Condition的 signal() 实现
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//用 Lock接口 实现 线程间定制化通信
//1.创建资源类 定义属性、方法
class ShareResource {
//定义标志位 A-1 B-2 C-3
private int flag=1;
//创建lock
private Lock lock=new ReentrantLock();
private Condition c1=lock.newCondition();
private Condition c2=lock.newCondition();
private Condition c3=lock.newCondition();
//2.在资源类操作方法
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=1) {
c1.await();
}
//干活
for(int i=1;i<=5;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=2;
//通知B
c2.signal();
} finally {
//解锁
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=2) {
c2.await();
}
//干活
for(int i=1;i<=10;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=3;
//通知C
c3.signal();
} finally {
//解锁
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag!=3) {
c3.await();
}
//干活
for(int i=1;i<=15;i++) {
System.out.println("第"+loop+"轮 "+Thread.currentThread().getName()+":"+i);
}
flag=1;
//通知A
c1.signal();
} finally {
//解锁
lock.unlock();
}
}
}
public class Test2_CustomizedThreadSignal {
//3.创建多个线程 调用资源类方法
public static void main(String[] args) {
//创建资源类对象
ShareResource resource=new ShareResource();
//创建多个线程
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for(int i=1;i<=10;i++) {
try {
resource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
}
}
5 集合的线程安全
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
//List HashSet HashMap 线程不安全
public class Test3_CollectionUnsafe {
public static void main(String[] args) {
//ArrayList线程不安全
// List<String> list=new ArrayList<>();
//Vector解决
// List<String> list=new Vector<>();
//Collections解决
// List<String> list= Collections.synchronizedList(new ArrayList<>());
//CopyOnWriteArrayList解决
List<String> list=new CopyOnWriteArrayList<>();
//HashSet线程不安全
// Set<String> set=new HashSet<>();
//CopyOnWriteArraySet解决
Set<String> set=new CopyOnWriteArraySet<>();
//HashMap线程不安全
// Map<String,String> map=new HashMap<>();
//Hashtable解决
// Map<String,String> map=new Hashtable<>();
//ConcurrentHashMap解决
Map<String,String> map=new ConcurrentHashMap<>();
for(int i=0;i<30;i++) {
String key=String.valueOf(i);
new Thread(()->{
//向集合添加内容
// list.add(UUID.randomUUID().toString().substring(0,8));
// set.add(UUID.randomUUID().toString().substring(0,8));
map.put(key,UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
// System.out.println(list);
// System.out.println(set);
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
5.1 ArrayList线程不安全
List<…> list=new ArrayList<>();
解决:
- Vector
List<…> list=new Vector<>();
- Collections集合工具类
List<…> list= Collections.synchronizedList(new ArrayList<>()); - CopyOnWriteArrayList
List<…> list=new CopyOnWriteArrayList<>();
读时共享,写时复刻
思想:拷贝一份
- 独占锁效率低:采用读写分离思想解决
- 写线程获取到锁,其他写线程阻塞
- 复制思想
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据
CopyOnWriteArrayList原理分析
- 动态数组 机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高
- 线程安全 机制
- 通过 volatile 和 互斥锁 来实现
- 通过 volatile数组 来保存数据. 一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证
- 通过 互斥锁 来保护数据. 在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的
5.2 HashSet线程不安全
Set<…> set=new HashSet<>();
异常报错是HashMap,因为HashSet底层基于HashMap实现:
Set元素唯一、无序的原因:
解决:CopyOnWriteArraySet
Set<…> set=new CopyOnWriteArraySet<>();
->
5.3 HashMap线程不安全
Map<String,String> map=new HashMap<>();
HashMap可存储null的key和value,null作为键只能有一个,null作为值可有多个
解决:
- HashTable
Map<String,String> map=new Hashtable<>();
HashTable使用synchronized来保证线程安全,在线程竞争激烈的情况下效率非常低下,所以HashTable基本被淘汰,不要在代码中使用它,要保证线程安全的话就使用ConcurrentHashMap
- ConcurrentHashMap
Map<String,String> map=new ConcurrentHashMap<>();
ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步
key和Value都不能为null,否则抛出 NullPointerException 异常(图片1011行)
ConcurrentHashMap原理 jdk7和8版本的区别
ConcurrentHashMap基于JDK1.8源码剖析
ConcurrentHashMap如何保证线程安全
6 多线程锁
6.0 锁的范围
锁的8种情况 (锁的范围-是否是同一把锁)
import java.util.concurrent.TimeUnit;
class Phone {
public static synchronized void sendSMS() throws Exception {
// public synchronized void sendSMS() throws Exception {
//停留 4 秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
// public static synchronized void sendEmail() throws Exception {
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
/**
* @Description: 8 锁 *
1 标准访问(2个同步方法),先打印短信还是邮件
------sendSMS
------sendEmail /锁的都是phone
2 停 4 秒在短信方法内,先打印短信还是邮件
(4s后打印)
------sendSMS
------sendEmail /锁的都是phone
3 新增普通的 hello 方法,先打印短信还是 hello
------getHello /没加锁 与锁无关
------sendSMS (4s后打印) /锁的是phone
4 现在有两部手机,先打印短信还是邮件
------sendEmail /锁的是phone1
------sendSMS (4s后打印) /锁的是phone
5 两个静态同步方法,1 部手机,先打印短信还是邮件
(4s后打印)
------sendSMS
------sendEmail /锁的都是Phone
6 两个静态同步方法,2 部手机,先打印短信还是邮件
(4s后打印)
------sendSMS
------sendEmail /锁的都是Phone
7 1 个静态同步方法(SMS),1 个普通同步方法(Email),1 部手机,先打印短信还是邮件
------sendEmail /锁的是phone1
------sendSMS (4s后打印) /锁的是Phone
8 1 个静态同步方法(SMS),1 个普通同步方法(Email),2 部手机,先打印短信还是邮件
------sendEmail /锁的是phone1
------sendSMS (4s后打印) /锁的是Phone **/
public class Test4_Lock8 {
public static void main(String[] args) throws Exception {
Phone phone=new Phone();
Phone phone1=new Phone();
new Thread(()->{
try{
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100); //线程什么时候创建不确定,所以在两线程中间睡眠一下,使效果更明显
new Thread(()->{
try{
// phone.getHello();
// phone.sendEmail();
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
结论
- 一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,即某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法. 因为锁的是当前对象 this(同一把锁),被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法
- 加个普通方法后发现和同步锁无关
- 换成两个对象后,不是同一把锁了,情况立刻变化
- 所有静态同步方法用的是同一把锁——类对象本身,和任何实例对象的普通同步方法用的锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的
⭐synchronized作用范围(3种同步方法)
作用范围 | 锁的对象 |
---|---|
普通方法 | 当前实例对象(对象锁) |
静态方法 | 当前类的 Class 对象(类锁) |
代码块 | synchonized 括号里配置的对象(对象锁) |
synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁 |
- 修饰普通方法
public class X {
//锁的是 this (同步方法使用的同步对象为该方法所属类本身的实例对象)
public synchronized void test() {
//code
}
}
- 修饰静态方法
public class X {
//锁的是 X.clss (类的字节码文件)
public static synchronized void test() {
//code
}
}
- 修饰代码块
public class X {
public void test() {
//锁的是 obj (可以是任意对象,但必须为同一对象)
synchronized (obj){
//同步代码块
}
}
}
6.1 非公平锁 & 公平锁
非公平锁
效率高 会导致线程饿死
卖票例子
ReentraintLock()无参构造默认用非公平锁
只有线程A占着锁,其它线程饿死
公平锁
效率相对低 资源对线程-雨露均沾 公平锁是为了让CPU发挥多线程的性能
卖票例子,ReentraintLock()有参构造传参
每个线程都能获得锁
6.2 可重入锁
synchronized隐式(内置)锁、Lock显式锁 都是可重入锁(递归锁)
可重入锁在破解第一把锁之后可一直进入到内层结构
演示可重入锁
public class Test5_ReentraintLock {
//2.同步方法演示可重入锁
public synchronized void add() {
add();//因为是可重入锁,所以会递归调用add();若是不可重入锁. 就会等待this对象释放锁,这段代码会死锁
}
public static void main(String[] args) {
new Test5_ReentraintLock().add(); //循环递归调用->最后栈溢出
//1.同步代码块演示可重入锁
Object o=new Object();
new Thread(()->{
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+":内层");
}
}
}
},"A").start();
//3.Lock演示可重入锁
Lock lock=new ReentrantLock();
new Thread(()->{
try {
lock.lock(); //上锁
System.out.println(Thread.currentThread().getName()+":外层");
try {
lock.lock(); //上锁
System.out.println(Thread.currentThread().getName()+":内层");
} finally {
lock.unlock(); //解锁
}
} finally {
lock.unlock(); //解锁
}
},"B").start();
new Thread(()->{
lock.lock(); //若是另一个线程不解锁 这个线程就得不到锁 一直等待不能执行
System.out.println(Thread.currentThread().getName());
lock.unlock();
},"C").start();
}
}
- 注释掉线程B的一个unlock:
6.3 死锁
参考
deadlock 多个进程互不相让,都得不到足够的资源(永久性阻塞)
演示死锁(记住 面试手撕代码)
import java.util.concurrent.TimeUnit;
public class Test6_DeadLock {
static Object a=new Object();
static Object b=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"持有锁a,试图获取锁b");
//睡眠1s让线程创建确定,使死锁效果更明显
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"获取锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"获取锁a");
}
}
},"B").start();
}
}
死锁验证方式
- jps [= Lunix: ps -ef 查询当前正在运行的进程;jdk提供的查看当前java进程的小工具,可看做 JavaVirtual Machine Process Status Tool 的缩写]
- jstack [JVM自带堆栈跟踪工具]
PATH配置了JAVA_HOME就可以在IDEA终端中使用命令
7 Callable接口
Callable接口创建线程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Thread1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " come in Runnable");
}
}
class Thread2 implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + " come in Callable");
return 200;
}
}
public class Test7_CallableVsRunnable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Runnable接口创建线程
new Thread(new Thread1(), "A").start();
//Callable接口创建线程 error->不能直接替换 Runnable,因为 Thread 类的构造方法没有 Callable
// new Thread(new Thread2(), "B").start();
//解决:用FutureTask替换Runnable
//FutureTask
FutureTask<Integer> futureTask1 = new FutureTask<>(new Thread2());
//Lambda
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + " come in Callable");
return 1024;
});
//创建线程
new Thread(futureTask2,"B").start();
new Thread(futureTask1,"C").start();
// while(!futureTask2.isDone()) {
// System.out.println("wait..");
// }
//调用FutureTask的get方法
System.out.println(futureTask2.get()); //之前做了各种计算,只计算一次
// System.out.println(futureTask2.get()); //第2次直接返回结果
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName() + " come over");
/** FutureTask原理 未来任务
* 不影响主线程的情况下,单开启线程去完成其它任务,主线程需要时直接get
*/
}
}
FutureTask原理
- 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 FutureTask 对象在后台完成,当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
- 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法;一旦计算完成,就不能再重新开始或取消计算. get 方法只有在计算完成时获取结果,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get 只计算一次,因此 get 方法放到最后
8 辅助类
参考
JUC 3 种常用辅助类 -> 解决线程数量过多时 Lock 锁的频繁操作
8.1 减少计数CountDownLatch
import java.util.concurrent.CountDownLatch;
/**
* 减少计数CountDownLatch -1
* 场景:6个同学陆续离开教室后班长才可以关门
*/
public class Test8_CountDownLatch {
public static void main(String[] args) throws InterruptedException {
//1.创建CountDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
for(int i=1;i<=6;i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "号同学离开教室");
//2.计数 -1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//3.等待 计数值减到0才会执行之后代码
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长锁门走人");
}
}
若不用CountDownLatch,班长锁门后还有同学被关在教室
使用CountDownLatch,保证所有人离开后才能锁门
8.2 循环栅栏CyclicBarrier
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 循环栅栏CyclicBarrier +1
* 场景: 集齐7颗龙珠就可以召唤神龙
*/
public class Test9_CyclicBarrier {
//1.创建固定值
private static final int NUM = 7;
public static void main(String[] args) {
//2.创建CyclicBarrier对象 设置固定值、达到值后要做的事
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM, ()->{
System.out.println("集齐7颗龙珠就可以召唤神龙");
});
for(int i=1;i<=7;i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + "星龙被收集");
//3.等待 +1 计数值+到7才会召唤神龙
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
若只循环到6没有达到7,则程序会一直运行(等待)
8.3 信号灯Semaphore
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* 信号灯Semaphore
* 场景: 6辆车抢3个停车位
*/
public class Test10_Semaphore {
public static void main(String[] args) {
//创建Semaphore对象,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆车
for(int i=1;i<=6;i++) {
new Thread(()->{
try {
//抢车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
//设置随机停车时间(5s内)
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName() + "离开车位---------");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放车位
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
9 读写锁
悲观锁 & 乐观锁
悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程). 因为不支持并发,所以效率低,适用于多写的应用类型
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现 - 悲观锁的实现,往往依靠数据库提供的锁机制. 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证. 但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会. 另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据
乐观锁
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制(version)和CAS算法(compare and swap比较与交换-无锁算法)实现. 主要步骤:冲突检测、数据更新
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁. 在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的 - 乐观锁不需要借助数据库的锁机制. 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁
读写锁
表锁:对整张表操作,不会发生死锁
行锁:对每张表单独一行进行加锁,会发生死锁
读锁:共享锁(可多人读),会发生死锁
写锁:独占锁(只一人写),会发生死锁
ReentrantReadWriteLock 读读共享 读写互斥 写写互斥
- 读锁 共享锁(shared locks) S锁
- 写锁 排它锁/独占锁(exclusive locks) X锁
读写锁3特性
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
- 重进入:读锁和写锁都支持线程重进入
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁
* 模拟缓存情景
*/
//创建资源类 定义属性、方法
class MyCache {
//创建 map 集合
private volatile Map<String,Object> map = new HashMap<>();
//volatile强制线程到主存而非自己的高速缓存中获取数据 因为数据经常被修改,不断发生变化
//创建读写锁对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//存数据
public void put(String key,Object value) {
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写.." + key);
//写一会儿
TimeUnit.MICROSECONDS.sleep(300); //睡眠300ms
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写完" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object value = null;
try {
System.out.println(Thread.currentThread().getName() + "读.." + key);
//读一会儿
TimeUnit.MICROSECONDS.sleep(300);
value = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return value;
}
}
//创建多个线程 调用资源类方法
public class Test11_ReadWriteLock {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//创建线程存数据
for(int i=1;i<=5;i++) {
final int num = i;
new Thread(()->{
// myCache.put(String.valueOf(i),i);//error 只能访问常量
myCache.put(num+"",num);
},String.valueOf(i)).start();
}
TimeUnit.MICROSECONDS.sleep(300); //使先写再读的效果更明显 不加这行运行结果也一样
//创建线程取数据
for(int i=1;i<=5;i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
不加读写锁,正常效果应该是写完后再读完:
加读写锁:
读写锁演变
读写锁降级
Linux命令中rwx(读/写/可执行)权限由低到高,可看出读锁权限<写锁权限
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test12_DemoteRWLock {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); //可重入读写锁对象
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); //读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级 写锁-降级->读锁
// //2.获取读锁
// readLock.lock();
// System.out.println("读..");
//1.获取写锁
writeLock.lock();
System.out.println("写..");
//2.获取读锁
readLock.lock();
System.out.println("读..");
//3.释放写锁
writeLock.unlock();
//4.释放读锁
readLock.unlock();
}
}
若先读后写,读锁不能升级为写锁,只有等读锁释放才能写:
可以先写后读,写锁降级为读锁:
小结
- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
- 原因:
当线程获取读锁时,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁
而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁
To be updated…