java多线程
基本概念描述
基本概念:
串行、并行和并发
串行:一个程序和冰糖葫芦一样,从头执行的尾
并发:统一时刻多个线程同事访问一个资源电商秒杀,春运抢票
并行:多个线程一起运行泡方便面,边烧水边撕开包装
进程/线程
什么是进程:进程是操作系统中资源分配的最小单位。是计算机中运行的一个应用,一个线程中可以由多个线程组成,例如qq中可以一边聊天一边打电话。
什么是线程:线程是操作系统中程序执行的最小单位,它依托于一个进程。前面的例子中聊天是一个线程,打电话是一个线程。
进程和线程的区别:
本质区别:
进程是操作系统资源分配的最小单位,而线程只是进程中执行的一个单位一段代码。
资源:
进程由自己独立的系统资源通过PCB(进程控制块管理)进程的资源是独立的,线程共享进程上的资源。
执行:
一个进程由自己的执行入口和出口可以独立执行,线程的执行必须依托于进程。
相互影响:
一个进程挂了,由于资源相互独立不会对其他进程有太大的影响,而线程中一个线程挂了,可能会引起死锁等严重的问题
切换:
进程比较重有自己的资源和PCB切换的开销大,线程本就共享资源切换开销小
线程的状态
新建(new)
可运行(Runable)
阻塞(Blocked)
waiting(不见不散)
timed_waiting(过时不候)
终止(Terminated)
一个线程进入runnable状态不代表它一定就执行,由于start方法的底层是一个start0方法该方法是一个native方法,需要由操作系统调度,还有就是不能重复调用start方法,调用一次该线程就处于一个Runnable状态了,再次调用抛出异常。
线程的四种创建方式
创建线程一共有四种方式分别为实现Runnable接口,继承Thread类,实现Callable接口,使用线程池创建
线程池在下面有介绍前两种不多赘述重点我们看看callable接口的实现,为什么可以使用callable创建,使用callable创建有什么好处
class Data implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1024;
}
}
public class ThreadPool {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> future = new FutureTask<Integer>(new Data());
new Thread(future,"AA").start();
Thread.sleep(1000);
System.out.println(future.get());
}
}
首先我们看上面的代码,可以看到传入Thread的是一个FutureTask类,那为什么说是实现Callable接口实现呢,看下图
看结构图FutureTask类变向实现Runnable,又通过构造方法传入一个Callable接口,这样将两个本就没有关系的接口给关联起来的设计模式称之为适配器模式。
解释完为什么可以使用它,我们就分析为什么要有这样一个东西,首先Runnable创建线程的缺陷在于它没有返回值,而Callable接口实现有返回值,这给我们的并发计算提供了很好的实现
如何编写企业级的代码:
固定的编程套路+模板:
1、在高内聚低耦合的前提下线程操作资源类
1.1创建一个资源类(高内聚,低耦合)
什么是高内聚低耦合:
空调的例子:
空调是资源类,资源类高内聚,低耦合,对外提供接口给外界操控。
对于java来说内聚方法和变量,
管程(Monitor):在操作系统中叫做监视器,在java中叫做锁是一种机制,在同一个时间只能由一个线程访问被保护的代码或者资源
用户线程和守护线程:
用户线程:我们平常创建的线程
守护线程:为用户线程服务的线程
守护线程可以由setDeamon方法来设置不过得在start方法之前
synchronized关键字:
synchronized是java中为了保护多线程操作的原子性而设计的一种锁机制底层使用monitor,实现一个非公平锁
可以干什么:
修饰一个代码块:修饰一个代码块可以锁对象也可以锁类。
修饰一个方法:修饰一个方法其实锁的是一个对象中所有用synchronized的方法
修饰一个静态方法:修饰一个静态方法锁的是整个类模板
lock接口
Lock和Synchronized的比较(五点):
①从根本上来说synchronized是java关键字是JVM层面上的底层使用Monitor实现,Lock是一个java类。
②使用上来说synchronized不需要我们主动释放锁,lock使用的时候需要我们自己释放锁,释放锁的操作放在finally代码块中保证锁一定释放避免死锁。
③公平性,sychronized默认实现不公平锁,lock可以根据构造方法来指定实现公平非公平
④lock支持中断和等待等待使用trylock(),中断使用interuptably(),而synchronized不支持
⑤lock是一种更加细粒度的锁,synchronized只能对对象或者对象的模板上锁,而lock可以通过condition实现更加细粒度的操作
线程之间的通信
线程编程的方法:
①创建资源类,在资源类中写入属性和操作的方法
②在资源类中操作方法判断(标志位)、干活、通知
③创建多个线程调用资源类中的方法
④防止虚假唤醒问题
例2:创建两个线程对线程加一,减一
在两个线程加,两个线程减时如果使用if来判断会出现虚假唤醒问题
例3:
A打印5,B打印10,C打印15
线程之间的定制化通信(按照约定的顺序执行线程):标志位
线程安全的集合
list集合的线程不安全全
解决方法:
Vector
Collections.synchronizedList()
CopyOnWriteArrayList原理:
写时复制技术:
Map和Set线程不安全:
CopyONWriteArraySet和ConcurrentHashMap解决该问题
多线程的常用工具类
Semaphore(信号量):它用于多线程操作多个资源类的调度,比如说一个停车场中有三个车位,现在有六辆车需要怎么调度
后面两种工具类都是用于计数的使用场景类似于多少个线程完成了任务才能接着执行下面的操作
CountDownLatch:一个递减的计数器
CyclicBarrier:递增的计数器
JMM(JAVA Memory Mode java内存模型)
java内存模型是一种多线程访问资源的规范,java内存模型规定java多线程访问数据时必须要先将主物理内存的数据拷贝到线程自己的内存中,修改之后,写回主物理内存中。
JMM规定java多线程访问资源类必须要有以下三大特性:
原子性:所有操作要么做要么不做
可见性:只要由一个线程修改了主物理内存的值就需要通知其他所有线程,这可以看做是一种通知机制
有序性:java编译器会对代码进行指令重排优化代码,但是其必须保证代码按照我们缩写的java代码意思执,指令重拍可能会破坏多线程情况下的程序执行顺序出现问题。
eg:
Volatile:
根据jmm的规定我们的多线程应该满足三大特性,但是volatile满足可见性,不满足原子性,禁止指令重排。
我们可以使用atomic包下的原子类来配合volatile使程序满足原子性
CAS(ComparAndSet):
cas是什么:是一种保证原子性的一种机制
eg:它描述的是,单线程操作资源类时携带一个期待值和一个修改值,如果一个线程要修改数据需要满足资源类中的值为期待值,才能修改成功。如果不是期待值需要重新将期待值修改为资源类中的值再次进行操作。
automicInteger的底层原理:
cas
cas底层原理;
automicInteger的方法底层都会调用unsafe类中的一个方法,unsafe类中的方法都是用native来修饰,代表着java需要调用操作系统中的原语来保证操作的原子性。
底层原语实现这样一个功能:携带对象和内存偏移量,找到数据,是用一个循环来判断是否与主物理内存的值相等,相等则退出循环写入,不相等重新加载执行一致的步骤
cas的缺点:
①一次只能保证一个对象的原子性
②循环损耗资源
③ABA问题:当两个线程同时访问资源类时,主物理内存的值为A,一个线程先修改主物理内存的值为B,再修改为A。另外一个线程再去操作资源类。
如何解决ABA问题:在juc包中不仅存在原子类操作,还可以定制化原子类(AutomicRefrence)。使用AtmicStampRefrence
使用一个计数器,每次修改主物理内存的值都会对计数器加一,来区别ABA中前后两个A(ABA出现的原因就是前后两个A不能区别开来)。
为什么不使用synchronize:太重,杀鸡不用牛到
线程不安全的集合
collection集合框架中有着两类的集合一类线程安全但是在支持多线程操作的情况下无法兼顾与效率,所以产生了线程不安全但是执行效率高的集合类型。java中线程安全集合由Vector,HashTable,Stack等等。线程不安全的集合有ArrayList,HashMap,HashSet等等。当使用多线程操作线程不安全的集合时会报一个ConcurrentModifiedException。
解决这些这些集合线程不安全的方法一共有三种:
①直接使用线程安全的集合
②使用Collertions中的synchronizedList,synchronizedSet,synchronizedMap解决
③使用JUC下的CopyOnWriteArrayLIst,CopyOnWriteSet,CopyOnWriteMap解决
它用到的主要是写时复制技术:写时复制技术是一种将读写分离的技术,当多个线程访问一份数据的时候,读取数据的线程还是读取原来的数据,写入的线程将原来的数据拷贝一份完成数据的修改,然后通知读的线程数据已经更新,需要来新的数据读取。
不安全代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uh7G04hk-1631352102155)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210908224400342.png)]
java中多种不同的锁
java中锁的设计思想乐观锁和悲观锁.
乐观锁:指的是总是考虑罪乐观的情况想着其他线程都不会来抢夺本线程所操作的资源,最经典的实现就是CAS。
悲观锁:悲观锁的思想就是考虑最悲观的情况,所有线程都会来抢夺本线程所操作的资源,最经典的实现就是ReentrantLock,阻塞其他线程。
java中为了适应不同的多线程环境设计了多种不同的锁主要包括如下几种:
重入锁和非重入锁:
重入锁指的是我们使用重入锁修饰的代码块,在该代码块内部可以任意使用该锁不需要重新获取,这是为了避免死锁。好比我们拿着家里的钥匙进入家门,不需要再获取家里的锁去卫生间。
重入锁代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4m1Sng4C-1631352102157)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210908224732720.png)]
非重入锁的概念与之相反
公平锁和非公平锁:
公平锁和非公平锁说的是各线程在竞争资源类时如何操作。对于公平锁来说顺序来获取先到先得最为经典的实现就是队列;而非公平锁则是任由线程疯抢。非公平锁的好处在于提高线程执行的性能,但是也会带来饥饿现象。
自旋锁:
自旋锁底层使用CAS(ComparAndSet)技术,底层使用一个循环来实现,线程不断比较自己的值和主物理内存中的值,直到期望值和主物理内存的值相等才写入。它的优点在于不用阻塞线程实现锁,但是缺点在于CAS技术的缺点(ABA现象,只能对一个对象上锁,循环影响效率)
代码实现:
读写锁,读锁(共享锁),写锁(独占锁)
读写锁应用场景是这样一个想象,想象我们在签名墙上签字时,既要保证所有人能看到签名墙上的名字又要保证正常写入。这涉及到读读不会影响数据的原子性,读写和写写都会影响。所以我们诞生了一个读写锁来解决这个问题。写锁又叫独占锁,某一个时刻只能由一个线程操作资源类(一个人的签名不允许被打断),读锁又称之为共享锁允许线程共同看到一个线程的值(允许多个人同时看到签名墙)。
代码实现:
class Test1{
Map map = new HashMap<String,Object>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void set(String key,String value) {
lock.writeLock().lock();
try {
System.out.println("开始写入");
map.put(key,value);
System.out.println("结束写入");
}finally {
lock.writeLock().unlock();
}
}
public String get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取数据" + map.get(key));
return (String) map.get(key);
}finally {
lock.readLock().unlock();
}
}
}
public class ReadWrite {
public static void main(String[] args) {
Test1 test = new Test1();
for(int i = 0;i < 10;i++) {
final int temple = i;
new Thread(()->{
test.set(String.valueOf(temple),UUID.randomUUID().toString());
},String.valueOf(i)).start();
}
for(int i = 0;i < 10;i++) {
final int temple = i;
new Thread(()->{
test.get(String.valueOf(temple));
},String.valueOf(i)).start();
}
}
}
JUC中常用的工具类
JUC中提供了三个常用的工具类,前两个CountDownLatch和CyclicBarrier用于计数(使用:多少个线程完成操作之后某一个线程才能操作,可以用于控制线程的执行顺序),前者递减计数后者递增。
第三个比较常用的工具类叫做Semaphore(信号量),用于多个线程访问多个资源时控制线程的访问,例如停车场中有三个车位有10辆车需要停车,线程如何调度。
代码:
阻塞队列
阻塞对类架构:Collection->Queue->BlokongQueue->七种实现
阻塞队列的方法表:
分为四组,抛出异常add,remove,element,没有异常offer,poll,peek,无限等待put,take定时等待offer,poll
加上时间和时间单位。
线程池
为什么使用线程池:
如果我们手动的创建许多的线程,容易引起操作系统奔溃,根据不同的操作系统可创建的线程数也有所不同,一般来说最多允许创建1024个线程。使用线程池,可以帮助我们管理线程,管理线程的资源,提高线程的响应速度。
线程池的基本架构和使用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDl1VtP8-1631352102162)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210911155440612.png)]
线程池的核心类ThreadPoolExecutor所有线程池的创建都是基于这个类实现的,线程池的工具类Executors中有四种默认创建线程的方式,在实际的工作中基本上都是不可以使用的是一种线程池的理想实现。原因在于SingleThreadExecutor和FixedThreadPool中将阻塞队列设置为LinkedQueue这是一个无限大的队列,对于性能的提高不大。而另外连个默认实现ScheduleThreadPool和CacheThreadPool将最大线程数设置为无限大,也失去了实战的意义(阿里巴巴开发规范)。
线程池的七大参数:
核心线程数:默认先开启几个线程工作
最大线程数:线程池最大能容纳的线程数
最大存活时间和后面的一个TimeUtil:指的是除了核心线程之外开启的线程多久时间未处理任务就关闭该线程
阻塞队列:线程池中如果需要处理的任务过多,会先让任务的等待,等待的任务放到阻塞队列中
拒绝策略:线程池无法再处理任务了怎么办,在java中共有四种任务的拒绝策略分别为抛出异常,返回所提交来任务的线程,删除队首和队尾的任务,让该任务入队。
线程池的执行流程:
线程池的执行流程大致如下,任务进入线程池,核心线程开启服务,如果核心线程全在服务则将任务放入阻塞队列,如果队列也满了就开启额外的线程服务,如果所有线程都在服务阻塞队列也满了还提交任务就启动拒绝策略。当所有任务处理完成之后除了核心线程以外的线程超过了最长的等待时间那么久关闭额外的线程,保留核心线程。
线程池的运行原理:银行窗口的例子