从b站狂神说Java处学习。
内容如下:
一、什么是juc(java.util.concurrent)并发编写
学习从:源码+官方文档
二、线程和进程
Java真的可以开启线程吗?
答:开不了
原因:看源码
thread的start方法调用了一个start0方法
native表示本地方法,底层C++,Java无法直接c操作硬件
private native void start0();
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 */
}
}
}
private native void start0();
看cpu核数的三种方法,1、代码2、任务管理器3、电脑管理器
回顾多线程
线程有六个状态 (不是五个)看Thread类源码
wait/sleep区别
比较点 | wait | sleep |
---|---|---|
来自不同的类 | Object | Thread |
关于锁的释放 | 会释放锁 | 不会释放 |
使用范围 | wait必须在同步代码块中 | 任何地方都可以睡 |
三、Lock锁(重点)
传统示范(不建议这么使用):要使用面向对象的思想把代码简化降低耦合度,写一个资源类(面向资源类对象)
传统的Synchroized方式
线程就是一个单独的资源类,没有任何附属操作:
真正的多线程开发,公司的开发,降低耦合性:
因为Runnable底层是一个函数式接口@FunctionaltInterface,(意思是接口只有一个方法)可以用lambda表达式简化匿名内部类
OOP编程就是对象 对象就是属性、方法
接下来进入正题Lock锁
进入到locks包下的lock接口,lock接口的使用方法
怎么使用接口里面的方法:实现类
接了来用lock锁实现刚刚的卖票
ReentrantLock(瑞全踢lock)
公平锁、非公平锁
公平锁:十分公平,可以先来后到
非公平锁:十分公平,可以插队(默认)
为什么默认是非公平锁,因为符合现实相对公平,比如一个3s可以执行完的线程排在3h的线程之后,那么难道还让那个3s的等3h然后再执行吗?
lock和synchronize的对比
比较点 | lock | synchronize |
---|---|---|
所处层面 | 是Java中的一个接口,它有许多的实现类来为它提供各种功能 | Java中的一个关键字,当我们调用它时会从在虚拟机指令层面加锁 |
获得锁的方式 | Lock的使用离不开它的实现类AQS,而它的加锁并不是针对对象的,而是针对当前线程的,并且AQS中有一个原子类state来进行加锁次数的计数 | 可对实例方法、静态方法和代码块加锁,相对应的,加锁前需要获得实例对象的锁或类对象的锁或指定对象的锁。说到底就是要先获得对象的监视器(即对象的锁)然后才能够进行相关操作。 |
获锁失败 | 使用Lock加锁的程序中,获锁失败的线程会被自动加入到AQS的等待队列中进行自旋,自旋的同时再尝试去获取锁,等到自旋到一定次数并且获锁操作未成功,线程就会被阻塞 | 加锁的程序中,获锁失败的对象会被加入到一个虚拟的等待队列中被阻塞,直到锁被释放;1.6以后加入了自旋操作 |
释放锁 | 1、可调用ulock方法去释放锁比synchronized更灵活;2、还可以使用trylock(尝试获取锁)方法,意思是没等到释放就结束了; | 不能指定解锁操作,1、执行完代码块的对象会自动释放锁2、占有锁的线程发生了异常,此时JVM会让线程自动释放锁3、调用wait方法,释放锁进入wating状态 |
造成死锁 | 而Lock必须在finally中主动unlock锁,否则就会出现死锁 | synchronized在发生异常时,会自动释放掉锁,故不会发生死锁现(此时的死锁一般是代码逻辑引起的),比如:1、线程1(获得锁1,【一秒后获得锁2】),线程2(获得锁2,【一秒后获得锁1】),然后就会一直等待;2、线程1(获得锁,阻塞)、线程2(等待,傻傻的等)造成死锁 |
效率方面 | 比较高 | 比较低 |
是否判断获得锁 | 可以判断获得锁 | 无法判断获得锁的状态 |
重入锁 | 都是可重入锁,不可中断,非公平锁 | 可重入锁,可以判断锁什么时候中断,非公平(可自己设置) |
适用范围 | 适合锁少量的代码同步 | 适合锁大量的代码同步 |
锁是什么,怎么判断锁的是谁?
四、生产者消费者问题
面试常问:单例模式、排序算法、生产者消费者问题、死锁
juc版本:lock
传统写法:Synchronized 版 wait notify
单独的资源类,降低耦合性
上面这样两个线程是没有问题的,但是如果有多个线程呢
问题存在:比如线程ABCD会怎样?
虚假唤醒
AC + BD -
输出:
注意上面if判断,if判断只判断一次,当A进去方法后,C也不会停,会继续进入方法,假设while判断,一旦被修改,一个拿到锁了,另一个会等待
原因:看官方api,object类中的wait方法
所以要循环判断,while
输出:
虚假唤醒发生的条件为:
1、当一个数据存在三个及以上的线程竞争访问时(必要条件)
2、至少有一个线程没有对数据进行加锁访问(充分条件,使得虚假唤醒发生可能)
当满足两个条件才可能发生虚假唤醒。仅仅是可能
解决虚假唤醒(以下任意一种方式即可):
1、所有的线程访问数据时都加锁
2、在循环中等待
但是用while会造成一个问题,因为在轮询切换时,可能会漏掉notify的中断,从而无休止的休眠下去,CPU消耗巨大
解决上面了但是仍然有问题,比如线程ABABCBABC…的随机顺序,如果我想有序执行ABCD该怎么办?
JUC版本的生产者消费者问题
之前我们已经了解到,lock锁代替synchroized
那么谁来代替wait和notify呢?
点击进去codition接口的await方法
进入Condition类,然后可以发现
使用方法:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally { lock.unlock(); }
}
}
重要的是三步:
//1、用锁去创建它
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
//等待await
notFull.await();
//唤醒signal
notEmpty.signal();
//唤醒全部
notFull.singalAll();
然后用上面那个示例,修改一下,-方法也改成跟这个一样
继续拿四条线程来试一下ABCD AC+ BD-
输出:
由此看出用lock不会产生虚假线程,但是仍然会有跟上面while循环的一样的线程执行顺序问题
是不是没有什么变化
但是任何一个新技术,绝对不是仅仅只是覆盖了原来的技术,优势和补充:
意思是使用notify和wait实现不了
Condition 精准的通知和唤醒线程
因为一个Condition(监视器)只能监视一个方法,然后通过监视器来监视我要唤醒的是哪个线程,所以刚刚监视了三个
/**
* A 执行完调用B,B执行完调用C,C执行完调用A
*/
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printC();
}
},"C").start();
}
}
class Data3{ // 资源类 Lock
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=1){
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
// 唤醒,唤醒指定的人,B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
// 业务,判断-> 执行-> 通知
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCCCCC");
// 唤醒,唤醒指定的人,c
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
输出:
A=>AAAAAAA
B=>BBBBBBBBB
C=>CCCCCCCCC
A=>AAAAAAA
B=>BBBBBBBBB
C=>CCCCCCCCC
A=>AAAAAAA
B=>BBBBBBBBB
C=>CCCCCCCCC
java.lang.IllegalMonitorStateException是在调用object的wait和notify,notifyAll方法的时候可能会出现的异常
官方说法:如果当前线程不是对象监视器(锁)的所有者。
理解:在synchronized中调用上述三种方法的对象,不是你用synchronized锁住的对象就会报错
Object lock = new Object();
synchronized(lock){
lock.wait();
}
如果是
Object lock = new Object();
synchronized(lock){
new Object().wait();
}
报错
五、八锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
并不是八锁只是为了深刻理解我们的锁
只是八道题:
- synchronized 锁的对象是方法的调用者(p1)!
- 两个方法用的是同一个锁,谁先拿到谁执行!(不能回答先调用,是先拿到锁)
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁,就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
* 2、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone p1 = new Phone();
//锁的存在
new Thread(()->{
p1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
p1.call();
},"B").start();
}
}
class Phone{
// synchronized 锁的对象是方法的调用者!、
// 两个方法用的是同一个锁,谁先拿到谁执行!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
- 两个对象,两个调用者,两把锁!
- hello这里没有锁!不是同步方法,不受锁的影响
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
* 4、 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
*/
public class Test2 {
public static void main(String[] args) {
// 两个对象,两个调用者,两把锁!
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone2{
// synchronized 锁的对象是方法的调用者!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
// 这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
- // static 静态方法 // 类一加载就有了!锁的是Class类 //一个类只有一个Class对象
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话? 1、发短信 2、打电话
* 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话? 1、发短信 2、打电话
*/
public class Test3 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone3{
// synchronized 锁的对象是方法的调用者!
// static 静态方法
// 类一加载就有了!锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
- 静态的同步方法 锁的是 Class 类模板
- 普通的同步方法 锁的调用者
/**
* 1、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话? 先打电话
* 2、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话? 先打电话
*/
public class Test4 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone4{
// 静态的同步方法 锁的是 Class 类模板
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通的同步方法 锁的调用者
public synchronized void call(){
System.out.println("打电话");
}
}
小结
new this 具体的一个手机(引用对象)
static Class 唯一的一个模板
六、集合类不安全
List不安全
- java.util.ConcurrentModificationException 并发修改异常!
面试的时候多说些这种异常,内存溢出、栈溢出、或者是上述提到的异常,一定是在实际代码过程中出现的异常
注意:这里用的是for循环包的线程,跟之前案例中线程方法里面接的for循环不一样,一个是创建多个线程,一个是线程内循环多次
ArrayList默认是线程不安全的,所以会报错。
解决方案:
- 用Vector就好了 (如果这么说很可能被问到,arraylist的add是怎么写的)
就是一个扩容的过程
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
arraylist的初始容量
private static final int DEFAULT_CAPACITY = 10;
但是ArrayList是jdk1.2出来的,而Vector在jdk1.0就出来了比ArrayList早,别人肯定会想到这个解决方案不要自作聪明,这个方案只要提一嘴就行了然后说他不推荐使用。
-
解决方案2,我们可以把arraylist变成安全的,数组有工具类叫Arrays,集合有工具类叫Collections
List list = Collections.synchronizedList(new ArrayList<>()); -
上面是普通人能想到的,接下来介绍JUC
List list = new CopyOnWriteArrayList<>();
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
// 在写入的时候复制了一份避免覆盖,造成数据问题!
// 读写分离
CopyOnWriteArrayList 比 Vector Nb 在哪里?
答:只要有synchronized的方法,相对效率比较低,Vector的底层add是synchronized修饰方法的,新版的是lock锁,并且是写入时复制一份再设置一份新的
package com.kuang.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
// java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
public static void main(String[] args) {
// 并发下 ArrayList 不安全的吗,Synchronized;
/**
* 解决方案;
* 1、List<String> list = new Vector<>();
* 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3、List<String> list = new CopyOnWriteArrayList<>();
*/
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
// 在写入的时候避免覆盖,造成数据问题!
// 读写分离
// CopyOnWriteArrayList 比 Vector Nb 在哪里?
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
学习方法推荐:1、先会用、2、货比3家,寻找其他解决方案,3、分析源码!
Set不安全
- //1、Set set = Collections.synchronizedSet(new HashSet<>());
- //2、Set set = new CopyOnWriteArraySet<>();
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 同理可证 : ConcurrentModificationException
* //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* //2、Set<String> set = new CopyOnWriteArraySet<>();
*/
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
// hashmap
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
// Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <=30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
hashset的底层有什么
public HashSet() {
map = new HashMap<>();
}
// add set 本质就是 map key是无法重复的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object(); // 不变得值!
HashMap不安全
回顾Map基本操作
初始容量16,加载因子0.75
解决方案:
- Collections工具类
- juc:concurrentHashMap(类似于上面的CopyOnWriteArraySet)
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
// ConcurrentModificationException
public class MapTest {
public static void main(String[] args) {
// map 是这样用的吗? 不是,工作中不用 HashMap
// 默认等价于什么? new HashMap<>(16,0.75);
// Map<String, String> map = new HashMap<>();
// 唯一的一个家庭作业:研究ConcurrentHashMap的原理
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}