JUC 高并发编程

0 导学

1 JUC概述

JUC概念

处理线程的工具包 java.util .concurrent 的简称

Java8API

进程、线程概念

1.1 进程 & 线程

进程线程
系统中正在运行的一个应用程序 / 运行的程序系统分配处理器时间资源的基本单元 / 进程内独立执行的一个单元执行流
资源分配的最小单位资源调度的最小单位 程序使用CPU的最基本单位
一个进程可并发多个线程每条线程并行执行不同的任务

1.2 线程状态

Thread.State 线程状态枚举类

在这里插入图片描述
线程状态转换图
在这里插入图片描述
参考

1.3 sleep & wait

sleepwait
所属类Thread静态方法Object方法 任何实例对象都可调用
时间指定时间可指定(限时等待)可不指定(无限等待)
释放锁释放CPU执行权 不释放同步锁释放CPU执行权、同步锁
使用的地方任何地方都能使用只能在同步代码方法/块中使用(调用前提:当前线程占有锁->代码要在 synchronized 中)
捕获异常必须捕获异常捕获/抛出异常
同:可被 interrupted 方法中断;在哪里睡,就在哪里醒

1.4 并发 & 并行

串行:所有任务按先后顺序执行
并行:同一时刻多个线程在访问同一个资源
并发(concurrent):多项工作一起执行,之后再汇总

1.5 管程

管程-Monitor监视器(OS) 锁(Java):是一种同步机制,保证同一时间,只有一个线程访问被保护数据/代码
JVM同步基于进入(加锁)和退出(解锁),使用管程对象实现(对临界区加锁和解锁)

1.6 用户线程 & 守护线程

用户线程:自定义线程

守护线程:如垃圾回收(后台执行)

2 Lock接口

2.1 synchronized、Lock 对比、介绍

synchronizedLock
存在层次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.卖票

⭐多线程编程步骤(高内聚 低耦合)

  1. 创建资源类 定义属性、方法
  2. 创建多个线程 调用资源类方法

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 线程间通信

线程间通信模型:共享内存、消息传递

⭐多线程编程步骤

  1. 创建资源类 定义属性、方法
  2. 在资源类操作方法(判断 干活 通知)
  3. 创建多个线程 调用资源类方法
  4. 防止虚假唤醒问题(判断条件要加到while中)

实现线程间交替+1-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
    }
}
  1. 用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<>();
在这里插入图片描述
解决:

  1. Vector
    List<…> list=new Vector<>();

  2. Collections集合工具类

    List<…> list= Collections.synchronizedList(new ArrayList<>());
  3. CopyOnWriteArrayList
    List<…> list=new CopyOnWriteArrayList<>();
    读时共享,写时复刻

思想:拷贝一份

  1. 独占锁效率低:采用读写分离思想解决
  2. 写线程获取到锁,其他写线程阻塞
  3. 复制思想
    当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
    这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据

CopyOnWriteArrayList原理分析

  1. 动态数组 机制
  • 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
  • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高
  1. 线程安全 机制
  • 通过 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作为值可有多个

在这里插入图片描述
解决:

  1. HashTable
    Map<String,String> map=new Hashtable<>();

HashTable使用synchronized来保证线程安全,在线程竞争激烈的情况下效率非常低下,所以HashTable基本被淘汰,不要在代码中使用它,要保证线程安全的话就使用ConcurrentHashMap

  1. 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 中的每一个对象都可以作为锁
  1. 修饰普通方法
public class X {
	//锁的是 this (同步方法使用的同步对象为该方法所属类本身的实例对象)
    public synchronized void test() {
    	//code
    }
}
  1. 修饰静态方法
public class X {
	//锁的是 X.clss (类的字节码文件)
    public static synchronized void test() {
    	//code
    }
}
  1. 修饰代码块
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();
    }
}
  1. 注释掉线程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();
    }
}

死锁验证方式

  1. jps [= Lunix: ps -ef 查询当前正在运行的进程;jdk提供的查看当前java进程的小工具,可看做 JavaVirtual Machine Process Status Tool 的缩写]
  2. 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中synchronizedReentrantLock等独占锁就是悲观锁思想的实现
  • 悲观锁的实现,往往依靠数据库提供的锁机制. 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证. 但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会. 另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据

乐观锁

  • 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制(version)和CAS算法(compare and swap比较与交换-无锁算法)实现. 主要步骤:冲突检测、数据更新
  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁. 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
  • 乐观锁不需要借助数据库的锁机制. 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

读写锁

在这里插入图片描述
表锁:对整张表操作,不会发生死锁
行锁:对每张表单独一行进行加锁,会发生死锁
读锁:共享锁(可多人读),会发生死锁
写锁:独占锁(只一人写),会发生死锁

ReentrantReadWriteLock 读读共享 读写互斥 写写互斥

  • 读锁 共享锁(shared locks) S锁
  • 写锁 排它锁/独占锁(exclusive locks) X锁

读写锁3特性

  1. 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
  2. 重进入:读锁和写锁都支持线程重进入
  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…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kukukukiki192

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值