前言:学习B站UP主狂神说视频笔记整理视频链接
什么是JUC
Thread:普通线程类
Runable:没有返回值,效率比Callable
相对较低
什么是JUC
JUC是
java.util.concurrent包名的简写,是关于并发编程的API。
与JUC相关的有三个包:
java.util.concurrent、
java.util.concurrent.atomic、
java.util.concurrent.locks。
线程和进程
进程是一个程序的集合 如QQ.exe
进程往往包含多个线程,至少包含一个
Java默认有几个线程
2个 ; main GC
Java真的可以开启线程吗?
开不了!
阅读源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);//加入队列
boolean started = false;
try {
//调用本地方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
//本地方法 调用底层C++ Java无法直接操作硬件
private native void start0();
并发,并行
并发:多个线程操作同一个资源
- CPU一核,模拟出来多线程,基于CPU时间片快速调度
并行:多个人一起行走
- CPU多核,多个线程可以同时执行
//在Java中可以通过此方法 查看CPU核心数
Runtime.getRuntime().availableProcessors()
并发编程的本质:充分利用CPU资源
多线程
线程六大状态
阅读源码可知,线程有六大状态
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//死亡
TERMINATED;
}
wait/sleep区别
1.来自不同的类
wait来着Object
sleep来着线程类
//在实际工作中,不会使用sleep让线程睡眠
//使用TimeUnit让线程睡眠
TimeUnit.DAYS.sleep(1);//睡一天
TimeUnit.SECONDS.sleep(2);//睡两秒
2.关于锁的释放
wait会释放锁
sleep不会释放锁
3.使用范围不同
wait必须在同步代码块中使用
sleep任何地方都可以睡眠
4.是否需要捕获异常
wait不需要捕获异常
sleep必须捕获异常
锁
synchronized
简介
synchronized
是解决线程不安全的关键,它的实现原理就是 队列和锁
由于我们可以通过private
关键词来保证数据变量(对象),只能被方法访问,所以我们只需要针对方法提出一套机制.这套机制就是synchronized
关键字.
实现原理
synchronized
方法控制对对象的访问,每个对象对应一把锁,每一个synchronized
方法都必须获得调用该方法对象的锁才能执行,否则会线程阻塞,方法一旦执行,就独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续往下执行
缺陷:如果一个大的方法被申明synchronized
将会影响效率
具体用法
它的用法有两种:
1.synchronized同步方法
//同步方法
public synchronized void method(int ages){
}
- synchronized方法 锁的是对象本身this
- 谁获得这个对象的锁才能执行这个方法
2.synchronized同步块
synchronized(obj){
}
同步块锁的是obj
Ojb称为同步监视器:
- 他可以是任何对象,但是推荐使用共享资源对象
- 同步方法种无需指定同步监视器,因为同步方法的同步监视器就是
this
就是这个对象的本身
Lock锁
简介
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition
。
Lock默认有三个实现类
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock(可重复锁),可以显式加锁、释放锁。
ReentrantLock()源码:
公平锁:先来后到,不可插队
非公平锁: 可插队
Lock简单案例
public class ThreadTest {
public static void main(String[] args) {
Tikect tikect = new Tikect();
new Thread(() -> {
for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
new Thread(() -> {
for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
new Thread(() -> {
for (int i = 0; i < 20 ; i++) tikect.numTest(); }).start();
}
}
class Tikect{
private Integer num =20;
//通过显示声明锁 进行手动加锁 和解锁
private Lock lock =new ReentrantLock();
public void numTest(){
lock.lock();//加锁
try {
if (num >0) {
num--;
System.out.println(num);
}
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
synchronized与Lock区别
- Synchronized 内置的Java关键字,Lock是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等) ; Lock锁就不一定会等待下去;
- Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,非公平(可以自己设置);
- Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
ReadWriteLock读写锁
ReadWriteLock只有唯一的一个实现类ReentrantReadWriteLock
//自定义缓存
class myCache{
private Map<String,Object> map = new HashMap<>();
//读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
//存的时候只有一个线程 存 取的时候可以多个线程取
//存
public void put(String key,Object value){
lock.writeLock();//写锁
try {
//业务代码
map.put(key,value);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();//释放锁
}
}
//取
public Object get(String key){
lock.readLock();//读锁
System.out.println(3);
try {
//业务代码
return map.get(key);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
return null;
}
}
生产者与消费者
传统synchronized版
public class ThreadTest {
public static void main(String[] args) {
Tikect tikect = new Tikect();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
tikect.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A");
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
tikect.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B");
}
}
class Tikect{
private int num =0;
public synchronized void increment() throws InterruptedException {
if (num!=0) {
this.wait();//睡眠
}
System.out.println(num++);
this.notifyAll();//唤醒睡眠的线程
}
public synchronized void decrement() throws InterruptedException {
if (num==0) {
this.wait();
}
System.out.println(num--);
this.notifyAll();
}
}
如果只是两个线程之间进行通信是没有问题的,如果多个线程之间四个五个六个之间并发执行就会产生虚假唤醒
因为if()
只会判断一次
我们应该根据官方文档,改成while()
循环判断,来防止虚假唤醒
class Tikect{
private int num =0;
public synchronized void increment() throws InterruptedException {
//防止虚假唤醒 使用while判断
while (num!=0) {
this.wait();//睡眠
}
System.out.println(num++);
this.notifyAll();//唤醒睡眠的线程
}
public synchronized void decrement() throws InterruptedException {
while (num==0) {
this.wait();
}
System.out.println(num--);
this.notifyAll();
}
}
Lock版
通过Condition替代了Object中睡眠唤醒方法
通过lock找到Condition
class Tikect{
private int num =0;
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try{
//防止虚假唤醒 使用while判断
while (num!=0) {
condition.await();//睡眠
}
num++;
condition.signalAll();//唤醒睡眠的线程
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try{
while (num==0) {
this.wait();
}
num--;
this.notifyAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
Condition实现精准通知唤醒,让线程顺序执行
public class ConditionTest {
// A 执行完 唤醒B B执行完 唤醒C 顺序唤醒
public static void main(String[] args) {
TestCondition condition = new TestCondition();
new Thread(() -> {
condition.prinltTest1(); },"A").start();
new Thread(() -> {
condition.prinltTest2(); },"B").start();
new Thread(() -> {
condition.prinltTest3(); },"C").start();
}
}
class TestCondition{
private Lock lock = new ReentrantLock();
//准备三个监视器
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int number =1;
//使用同一把锁进行加锁
public void prinltTest1(){
lock.lock();
try {
// 判断是否执行-> 执行具体业务 -> 唤醒其他线程
while (number!=1){
condition1.await();//睡眠
}
//业务
number++;
System.out.println("A执行了");
//唤醒B
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void prinltTest2(){
lock.lock();
try {
// 判断是否执行-> 执行具体业务 -> 唤醒其他线程
while (number!=2){
condition2.await();//睡眠
}
//业务
number++;
System.out.println("B执行了");
//唤醒C
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void prinltTest3(){
lock.lock();
try {
// 判断是否执行-> 执行具体业务 -> 唤醒其他线程
while (number!=1){
condition3.await();//睡眠
}
//业务
number=1;
System.out.println("C执行了");
//唤醒A
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
实现精准唤醒-> 需要准备多个监视器,通过condition1.signal();
唤醒指定的监视器
集合类不安全
在多线程下操作集合容易出现
java.util.concurrentModificationException并发修改异常!
List
在多线程并发情况下,ArrayList
不再具有安全性,此时我们应该使用安全的List
解决方案一:Vector
使用线程安全的类
//解决方案:
List<String> list = new Vector<>();
Vector在底层源码中使用synchronized
来进行修饰的.,所以它是线程安全的
解决方案二:Collections工具类
使用工具类转化线程不安全的类
//Collections转化线程安全的类
List<String> list = Collections.synchronizedList(new ArrayList<>());
解决方案三:CopyOnWriteArrayList
CopyOnWriteArrayList是JUC下线程安全的类
List<Object> objects = new CopyOnWriteArrayList<>();
CopyOnWrite的意思是:
写入时复制List 简称COW,它是计算机程序设计领域的一种优化策略
多个线程调用时,List读取的时候,固定的,产生写入(覆盖)
copyOnWrite 的解决方案是 在写入时进行复制 避免覆盖 造成的数据问题
CopyOnWriteArrayList比Vector好在哪里呢?
我们查看底层源码可知:
在CopyOnWriteArrayList中使用的是高性能lock锁
Set
hashSet的底层是什么?
查看底层源码会发现hashSet本质上是一个hashMap
add方法的本质就是往map中put值
解决方案一:Collections工具类
与list相似,我们可以使用工具类的方式来转换线程不安全的类
Set<Object> set = Collections.synchronizedSet(new HashSet<>());
解决方案二:CopyOnWriteArraySet
Set<Object> set1 = new CopyOnWriteArraySet<>();
Map
解决方案一:Hashtable
Map<Object, Object> objectHashtable = new Hashtable<>();
HashTable是线程安全的:
HashTable和HashMap的实现原理几乎一样,
差别:1.HashTable不允许key和value为null;
HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞
解决方案二:Collections工具类
Collections.synchronizedMap(new HashMap<>());
解决方案三:ConcurrentHashMap
Map<Object, Object> map = new ConcurrentHashMap<>();
JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,总结如下:
1、JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)
2、JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了
3、JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
4、JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:
- 低粒度加锁方式,synchronized并不比ReentrantLock差,
粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了 - JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
- 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
Callable
简介
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。
Callable区别:
1.有返回值
2.可以抛出异常
3.方法不同 run()/call()
测试使用
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCall call = new MyCall();
//寻常 多线程是如下方式开启 ,但是在Thread中不能直接传入Callable
/* new Thread(new Runnable() {
@Override
public void run() {
}
}).start();*/
//那么如何执行Callable呢?
//我们知道在Runnable的实现类FutureTask 构造方法中可以传入Callable 于是可以如下使用
FutureTask<String> futureTask = new FutureTask<>(call);
new Thread(futureTask).start();
//如何获取结果呢?
String s = futureTask.get();//使用get参数 获取获取结果
//如果多条线程执行呢?
new Thread(futureTask).start();
new Thread(futureTask).start();
//会发现方法打印只执行了一次
//原因是Callable有缓存
}
}
class MyCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("执行方法");
//调用结果时可能会产生阻塞,因为方法内部逻辑的执行是一个耗时操作
return "ok";
}
}
常用辅助类
CountDownLatch减法计数器
public class Test1 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(">>>"+Thread.currentThread().getName());
count.countDown();//数量-1
},String.valueOf(i)).start();
}
count.await();//等待计数器归零后才往下执行
System.out.println("这段代码会等待所有线程执行完毕以后,再执行");
}
}
CyclicBarrier加法计数器
CyclicBarrier
有两个构造方法,一个只用于计数,一个是计数完可以执行一个线程
public class Test2 {
public static void main(String[] args) {
//计数器加到7时 执行线程
CyclicBarrier barrier = new CyclicBarrier(7,() -> {
System.out.println("计数完执行的线程");
});
//创建7个线程
for (int i = 0; i < 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
try {
barrier.await();//线程等待 计数器+1
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
注意点:如果无法计数到指定数量,那么线程会卡死,一直等待下去
Semaphore信号量
public class Test3 {
public static void main(String[] args) {
//Semaphore 主要应用于限流
Semaphore semaphore = new Semaphore(3);
//Semaphore的位置只有3个 现在有6个线程 先拿到通行证的线程先执行 其他线程必须等待释放以后执行
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();//获取通行证
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(2);
System.out.println("释放通行证");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放
}
},String.valueOf(i)).start();
}
}
}
阻塞队列
BlockingQueue
BlockingQueue不是一个新东西,它是跟List,Set同级的集合框架
四组API
方式 | 抛出异常 | 有返回值,不抛异常 | 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put | offer(“a”,2, TimeUnit.SECONDS) |
移除 | remove | poll() | take | poll(2,TimeUnit.SECONDS); |
判断队列队首 | element | peek |
抛出异常
//抛出异常
public static void a(){
//构造方法 需要传入队列大小
ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);
//往队列放入
b.add("a");
b.add("b");
b.add("c");
//队列已满 如果继续放会报错 java.lang.IllegalStateException: Queue full 队列已满异常
//b.add("d");
b.element();//查看队首的对象是谁
b.remove();//弹出
b.remove();
b.remove();
//队列已空 如果继续取出 会报错 java.util.NoSuchElementException
// b.remove();
}
不抛出异常,有返回值:
b.element();//查看队首的对象是谁
public static void b(){
//构造方法 需要传入队列大小
ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);
//往队列放入
b.offer("a");
b.offer("a");
b.offer("a");
//队列已满 继续放入 返回值为false 没有异常
b.offer("a");
b.peek();//查看队首
b.poll();
b.poll();
b.poll();
//队列已空 如果继续取出 为null 没有异常
b.poll();
}
等待
//等待 阻塞(一直阻塞)
public void c() throws InterruptedException {
//构造方法 需要传入队列大小
ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);
//往队列放入
b.put("a");
b.put("a");
b.put("a");
//b.put("a"); 队列已满 一直阻塞
b.take();
b.take();
b.take();
// b.take(); 队列已空 一直阻塞
}
等待超时
//等待 超时等待
public void d() throws InterruptedException {
//构造方法 需要传入队列大小
ArrayBlockingQueue<Object> b = new ArrayBlockingQueue<>(3);
//往队列放入
b.offer("a");
b.offer("a");
b.offer("a");
b.offer("a",2, TimeUnit.SECONDS);//超时等待两秒 两秒之后退出
//取出
b.poll(2,TimeUnit.SECONDS);
b.poll(2,TimeUnit.SECONDS);
b.poll(2,TimeUnit.SECONDS);
b.poll(2,TimeUnit.SECONDS);//超时等待两秒 取不到值就退出
}
SynchronousQueue
没有容量,
进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
public static void main(String[] args) {
//同步队列
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
queue.put("");
System.out.println("存");
queue.put("");
System.out.println("存");
queue.put("");
System.out.println("存");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
queue.take();
System.out.println("取");
queue.take();
System.out.println("取");
queue.take();
System.out.println("取");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}