线程知识详解
线程
线程常见问题及操作
进程是操作系统资源分配的基本单位,线程是操作系统调度运行的基本单位
- run方法和start方法的区别:
run方法中说明了当前的线程的任务清单,当创建该线程对象后调用start方法会调用jvm虚拟机底层的程序创建线程并执行run中的方法
- 中断线程的方法:
- 通过共享的标记进行信号传递,进行线程的中断控制
- 调用Thread中的interrupt()方法进行线程的中断操作会抛出InterruptException异常
当前线程在等待,睡眠和阻塞的情况下可以使用中断函数进行线程中断并在抛出的InterruptedException中进行对应的操作
中断方法面对线程的使用逻辑以及底层执行流程
中断方法只有当前线程在等待,睡眠,和阻塞的情况下可以使用中断方法中断当前的线程执行,此处我将线程的运行逻辑分为有锁中断和无锁中断:
无锁中断:当前线程在正常等待,睡眠和阻塞的情况下,退出当前状态执行中断异常处理中对应的代码逻辑
有锁中断:当前线程如果持有锁的情况下没有进入到阻塞状态,这是我们采取中断方法会失效,线程继续执行当前逻辑
如果当前线程在持有锁的情况下执行wait进入到阻塞状态并释放锁:这个时候使用线程的中断方法进行线程中断,线程从阻塞队列中退出来,执行中断异常中的代码逻辑,如果最后锁未释放,则释放锁
- 线程的重要方法:
- 抢占方法join(),参数可以设置最大时间上限
- 获取当前的线程currentThread()
Thread test=Thread.currentThread();
//例子,获取当前线程的名称
Thread.currentThread().getName();
- State:是线程封装类中的一个枚举(其中存放了所有的线程的所有状态):
Thread.State[] values = Thread.State.values();
- 线程的休眠方法sleep()
- 线程的状态
创建:线程对象实例化
运行:线程对象执行start方法进行运行,由系统调度执行调度状态的线程
就绪:有线程调度机制将线程改变为就绪状态,通过礼让方法yield方法进入到就绪状态
等待:wait方法,park方法join方法使得当前线程进入到等待状态
超时等待:sleep,wait,join,parkNanos,parkUntil这些带最大超时时间参数的方法进入到超时等待
阻塞:等待进入到同步代码块中的等待获得锁,进入到等待状态
完成:执行完成进入到终止在状态
- park方法和parkNanos和parkUntil方法的区别
三个方法都是由LockSupport类实现的静态方法,使用park会使线程进行到阻塞状态,等待unpark方法唤醒,parkNanos进行相对时间等待,在等待时间之后会进入到下一次的线程调用之中,parkUntil给定一个指定时间,时间到了如果还没有被唤醒,就会在到了指定时间之后进行唤醒
wait方法和park方法的区别
wait方法:必须在同步代码块中进行使用,会使当前线程释放锁,等待notify方法进行唤醒,由Object类进行调用
park方法:不需要必须在同步代码块中进行使用,会使得当前线程阻塞,等待unpark方法唤醒线程,由LockSupport类进行调用
线程的安全性问题
- 导致线程不安全的原因:
- 线程之间可抢占
- 没有保证程序的原子性
- 存在共享数据的修改问题
- 没有保证内存的可见性:内存的可见性就是一个线程修改数据之后其他线程能够及时得到消息
- java中的内存模型jmm内存模型:
每个线程都有一个对应的工作内存,每次线程读取数据的时候,都是读取工作内存上的副本数据,当线程要对数据进行修改的时候,都是先修改工作内存中的副本数据,然后再将副本数据同步到工作内存当中去,工作内存指的是CPU寄存器和高速缓存
- 指令重排序
-
指令重排序(Instruction Reordering)是编译器和处理器为了优化程序性能而采取的一种技术。它通过改变指令的执行顺序,使得程序在不影响最终结果的前提下,能够更高效地利用CPU资源。指令重排序主要发生在两个层面:编译器优化和处理器执行。
-
数据依赖性和内存屏障,指令重排序虽然能够提高程序的执行效率,但也可能导致多线程程序中的数据竞争和不一致问题。为了保证多线程程序的正确性,Java 提供了内存屏障(Memory Barrier)机制,用于控制指令的重排序。
LoadLoad屏障:禁止读操作之间的重排序。
StoreStore屏障:禁止写操作之间的重排序。
LoadStore屏障:禁止读操作和写操作之间的重排序。
StoreLoad屏障:禁止写操作和读操作之间的重排序。
理解内存屏障:比如读写操作进行重排序在多线程环境下会导致错误,这个时候就需要进行内存排序,内存排序相当于一堵墙,比如说读写操作,在读写操作中插入一堵墙,在写操作之后的的任何操作不能跨越这堵墙,达到隔离的状态
- 聊一聊synchronized
synchronized的运用:可以创建非静态同步代码方法,静态同步代码方法,非静态同步代码块,静态同步代码块
public class test{
//同步代码块的Object
Object obj=new Object();
//静态同步代码方法,使用的锁对象是当前类的class方法
public static synchronized void test1(test.class){
System.out.println("使用静态同步代码方法");
}
//非静态同步代码方法,使用的锁对象是this对象
public synchronized void test2(this){
System.out.println("使用非静态同步代码方法");
}
//静态同步代码块,使用的锁对象可以是静态成员变量和当前类的class对象
public void test3(){
synchronized (obj){
System.out.println("运行非静态同步代码块");
}
}
public static void test4(){
synchronized(test.class){
System.out.println("运行静态同步方法块");
}
}
}
synchronized的底层执行流程:
- 线程获取到当前同步代码块中的锁
- 将主内存中的数据副本到线程工作内存当中
- 执行代码程序
- 将线程工作内存中的数据刷新到主内存中
- 释放当前代码块中的锁
synchronized使用的操作系统底层的mutex lock实现的锁,保证程序执行的原子性,synchronized是可重入锁
- volatile关键字
jmm内存模型会导致内存的不可见性:线程将主内存中的数据副本到工作内存中,当其他线程进行修改时,这个线程还是在工作内存中读取原始副本
而volatile修饰的变量,强制性进行主内存的读写,
在写入数据时,改变工作内存中的数据,再刷新到主内存当中
在读出数据时,首先重主内存中读取数据到工作内存中,再从工作内存中读取数据
- 什么情况下会释放锁
- 线程执行的同步代码方法结束之后会释放锁
- 同步代码方法执行过程中遇到break,return会释放锁
- 同步代码中出现了未处理的Error和异常Exception会导致异常结束
- 在同步代码块中遇到方法wait方法会释放锁
- 讲一讲wait方法和notify方法
- wait有参数类对象进行调用,是Object类中的方法
//wait方法的工作流程
1. 首先将当前线程进入到等待队列中执行等待
2. 释放当前的锁
3. 满足一定的条件时被唤醒,重新尝试获取这个锁
public void test(){
synchronized(obj){
System.out.println("执行方法");
obj.wait();
System.out.println("等待结束");
}
}
注意这里只是一个抽象实例,wait需要与notify方法匹配使用,如果要将wait方法和notify方法写在同一个同步代码块之中,建议将notify方法写在finally中,这样保证不会有线程一直处在阻塞状态
- notify方法唤醒正在等待的线程,注意在里一个线程中唤醒另一个正在等待的线程,先要等当前线程执行完成后另一个线程才能够改变状态运行
- 在唤醒的时候并不是按照等待的线程中的先后顺序进行唤醒的,而是线程调度器随机挑选进行唤醒的,notifyall方法将会唤醒所有的线程等待线程调度器的调度
- 线程在定时器的应用
Timer定时器任务类,可以创建一个单独的线程进行定时任务的处理,TimerTask承载定时器的run方法
public void test(){
Timer timer=new Timer();
timer.schedule(new TimerTask(){
@Overide
public void run(){
System.out.println("定时器执行");
}
},4000);
}
当使用Timer类创建对应的定时器任务的时候,系统会自动创建一个新的线程来实现这个定时器任务
- 单例模式中的饿汉模式
如果我们的一个类整体作为一个共享变量,在多线程的过程中这个类只产生一个实例,我们一个怎么操作,就需要用到单例设计模式:就是一个类只有一个实例:
- 分为饿汉模式:
public class Etest{
public static Etest etest=new Etest();
private Etest(){}
public static Etest getIntence(){
return etest;
}
}
- 对饿汉模式进行优化,不在类加载的时候创建实例,在使用的时候才创建实例,我们采用懒汉模式
public class Etest{
public static Etest etest=null;
private Etest(){}
public static Etest getIntence(){
if(etest==null){
etest=new Etest();
}
return etest;
}
}
- 这个时候我们开始思考一个问题,当多个线程进行懒汉模式创建的时候,如果同一时刻都访问到这个,两个线程在进行判断的时候etest都是null,都进行etest的创建,这个时候就造成了两个实例的创建,我们可以对创建etest的实例进行线程安全性优化
public class Etest{
public static volatile Etest etest=null;
private Etest(){}
public static Etest getIntence(){
if(etest==null){
synchronized(){
if(etest==null)
etest=new Etest();
}
}
return etest;
}
}
当线程需要进行是否创建判断的时候再次进行加锁判断,将实例设置成volatile变量的时候,保证内存可见性,优化线程安全性
- 阻塞队列
java的阻塞队列BlockingQueue的实现类类型:
ArrayBlockingQueue:底层基于数组的阻塞队列
LinkedBlockingQueue:底层基于双向链表的阻塞队列
PriorityBlockingQueue:底层基于堆的阻塞队列,支持优先级排列的无界阻塞队列
当线程进行等待的时候进入到阻塞队列
- 定时器任务:阻塞队列的应用
java封装的定时器执行类timer底层维护的就是一个阻塞队列
public class Test{
public void test(){
Timer timer=new Timer();
timer.schedule(new TimerTask(){
System.out.println("定时器执行");
},30);
}
}
Timer类底层维护的是优先级阻塞队列PriorityBlockingQueue,根据设定的等待时间,系统计算出应该执行的时间进行,按照应执行时间进行优先级的排列
- 阻塞队列的常用方法:
插入元素:
boolean add(E e)
: 将指定元素插入队列,如果队列已满,则抛出IllegalStateException
。boolean offer(E e)
: 将指定元素插入队列,如果队列已满,则返回false
。void put(E e) throws InterruptedException
: 将指定元素插入队列,如果队列已满,则阻塞直到有空间可用。boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException
: 将指定元素插入队列,如果队列已满,则等待指定时间,如果在指定时间内仍无法插入,则返回false
。
获取元素:
E take() throws InterruptedException
: 获取并移除队列的头元素,如果队列为空,则阻塞直到有元素可用。E poll(long timeout, TimeUnit unit) throws InterruptedException
: 获取并移除队列的头元素,如果队列为空,则等待指定时间,如果在指定时间内仍无法获取元素,则返回null
。E poll()
: 获取并移除队列的头元素,如果队列为空,则返回null
。E remove()
: 获取并移除队列的头元素,如果队列为空,则抛出NoSuchElementException
。
检查元素:
E peek()
: 获取但不移除队列的头元素,如果队列为空,则返回null
。E element()
: 获取但不移除队列的头元素,如果队列为空,则抛出NoSuchElementException
。
常用方法:
int size()
: 返回队列中的元素数量。boolean isEmpty()
: 判断队列是否为空。boolean contains(Object o)
: 判断队列是否包含指定元素。
线程池
线程池的优势:
通过线程池创建线程,在性能优化上比直接调用API接口创建线程更好,避免重复的创建和销毁,降低资源消耗,提高相应速度,可管理性
通过线程池创建线程的demo:
public class Test{
public void test(){
//创建固定长度的线程池
ExecutorService service=Executors.newFixedThreadPool(10);
//向其中注册线程
for(int i=0;i<1000;i++){
service.submit(()->System.out.println("你好"));
}
}
}
ExecutorService类是线程任务的操作类的封装,Executors提供很多静态方法用来创建线程池
线程的几种类型:
固定数量的线程: Executors.newFixedThreadPool()
缓存线程池(数目动态增长):Executors.newCachedThreadPool()
创建唯一线程的线程池:Executors.newSingleThreadExecutor()
执行定期延时时间执行:Executors.newScheduledThreadPool()
适用于并行处理的线程池(基于工作窃取算法):Executors.workStealingPool()
- 并行处理的线程池workStealingPool的底层逻辑
在workStealPool线程池中每一个线程都维护一个工作线程的任务队列,每个线程的任务队列是相互独立的,可以并行处理,工作窃取算法就是当假如一个线程执行完成后,从别的线程的任务队列尾部窃取任务到自己的任务队列中进行执行,从他的工作逻辑可以看出,当任务量较多的时候可以采用这种线程池进行执行,将线程进行负载均衡,提高任务的执行效率
这是创建线程池的一种方法:Executors实际上是对ThreadPoolExecutor类的一种封装,ThreadPoolExecutor可以创建线程
- ThreadPoolExecutor创建线程的参数
- int corePoolSize:核心线程数
- int maxmumPoolSize:最大线程数
- Long keepAliveTime:允许线程的最大空闲等待时间,超过时间线程销毁
- TimeUnit unit:等待时间对应的空闲等待时间的单位
BlockingQueue<Runnable> workQueue
:管理线程的阻塞队列,线程池可以内置一个阻塞队列,也可以自己手动指定一个- ThreadFactory threadFactory:创建线程的工厂
- RejectedExecutionHandler handler:拒绝策略,当线程池中阻塞队列满时,继续添加线程的对应处理方案
- RejectedExecutionHandler拒绝策略方案:
- AbortPolicy:直接抛出异常,程序中断,其他线程也停止运行
- CallerRunsPolicy:添加这个任务的线程去执行这个任务
- DiscardOldestPolicy:丢弃掉最早的线程,执行新的线程
- DiscardPolicy:将添加的这个新的任务直接丢弃
//核心线程数
int centerThreadsum=2;
//最大线程数
int maxThreadsum=10;
//通过ThreadPoolExecutor创建线程
TheadPoolExecutor test=new ThreadPoolExecutor(centerThreadsum,maxThreadsum,1000,TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
通过七个参数创建自定义的线程池
八股 —线程池的底层执行逻辑
执行的过程中,首先判断核心线程是否有空闲,没有的话先入阻塞队列,当阻塞队列也满的时候考虑辅助线程,如果都满的话就会执行拒绝策略。
锁
锁根据分类可以分为一下几种类别:
- 乐观锁和悲观锁
乐观锁:默认不会遇到多线程安全问题,常用的版本号法
悲观锁:一般默认所有的数据都会被修改,将所有的操作放在synchronized中
- 轻量级锁和重量级锁
轻量级锁和重量级锁的判定要看是不是运用底层操作系统的mutex互斥锁
底层逻辑:在计算机硬件上的原子性操作,操作系统将在硬件原子性操作的基础上实现了mutex互斥锁,JVM又在操作系统的基础上封装了mutex实现了synchronized和ReentrantLock这样的互斥锁
轻量级锁:版本号法就是轻量级锁的运用,占用资源较少
重量级锁:依赖底层的mutex,开销较大,占用的资源较多
- 自旋锁和挂起等待锁
这个不是指向特定的锁,而是一种根据状态进行的分类,线程在没有获取锁的时候会进入到阻塞队列中,当释放锁之后,由JVM的线程调度器进行下一步的线程调用
挂起等待锁:等待由线程调度器调用
自旋锁:从代码状态持续获取锁,避免线程调度器进行调度,但是会持续占用CPU资源
//自旋锁
public class zixuan {
//自旋锁的模拟封装类
private AtomicReference<Thread> lock=new AtomicReference<>();
//自旋锁的获取锁操作
public void lock(){
//调用当前方法的线程
Thread thread = Thread.currentThread();
//自旋获取锁
while(!lock.compareAndSet(null,thread)){
System.out.println("正在循环获取锁中请耐心等待....");
}
}
//释放锁的模拟方法
public void unlock() throws IllegalAccessException {
//获取调用释放锁对象的线程
Thread thread = Thread.currentThread();
//尝试释放锁
if(lock.compareAndSet(thread,null)){
System.out.println("成功释放锁");
}
else{
throw new IllegalAccessException("当前线程没有获取锁,不能释放锁");
}
}
//对应的操作
}
- 读锁和写锁
ReentrantReadWriteLock:读锁,ReentrantWriteLock:写锁
读锁和读锁之间不存在互斥,写锁和写锁之间互斥,读锁和写锁之间互斥
读写锁大大提高了多线程并时的效率
//读写锁demo
public class ReadWriteTest {
private volatile int valiue=0;
//创建读写锁对象
private final ReadWriteLock lock=new ReentrantReadWriteLock();
//读操作
public void read(){
//获取读锁
lock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"读取数据"+valiue);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放读锁
lock.readLock().unlock();
}
}
//写操作
public void write(int value){
//获取写锁
lock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"写入数据"+value);
this.valiue=value;
}catch (Exception e){
e.printStackTrace();
}finally {
//释放写锁
lock.writeLock().unlock();
}
}
}
- 公平锁和非公平锁
按照是否按照线程进入顺序获取锁分为公平锁和非公平锁,JVM线程调度本身是随机调用的
- 可重入锁和不可重入锁
一个线程是否可以重复获取一个锁,比如说递归函数中重复获取同一把锁
- 线程死锁条件:
- 互斥:各个线程之间对数据的操作是互斥的,不能同时进行
- 不可抢占:线程之间不能够抢占修改数据
- 请求和保持:线程会持续等待获取锁
- 循环等待:多个线程形成一个线程等待环路
- 创建线程的方法:
- 继承Thread
- 实现Runnable接口
- 使用线程池
- 使用Callable(对run方法的改进,run方法可以赋予返回值,Callable与FutureTask搭配使用)
//Callable的demo
public static void main(String[] args){
Callable<Integer> callable=new Callable<Integer>(){
@Override
public Integer call() throws Exception{
int sum=0;
for(int i=1;i<=1000;i++){
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futuretask=new FutureTask<>(callable);
Thread thread=new Thread(futuretask);
thread.start();
int result=futureTask.get();
System.out.println(result);
}
八股-CAS问题
CAS是硬件本身对数据处理的原子性操作,多个线程同一时刻只允许一个线程对数据进行处理,处理的时候先判断线程寄存和内存中数据是否一致,一致的时候在进行修改,而java对硬件的CAS操作进行了封装,可以在代码阶段运用CAS,而CAS会导致ABA问题,封装通过版本号法解决了ABA问题,实现了完美的无锁编程,但是CAS只能够实现单个数据的原子性,不能保证代码块的原子性
CAS的java封装demo
CAS操作对应的原子类:
AtomicInteger,AtomicBoolean,AtomicIntegerArray,AtomicLong,AtomicReference,AtomicStampedReference这些数据封装进行原子性操作
//CAS的demo
public class CASTest{
//AtomicInteger是CAS底层的封装类,这里面对int型的0进行原子性操作
private static final AtomicInteger couter=new AtomicInteger(0);
public void test(){
//创建十个线程实现对值的操作
for(int i=0;i<10;i++){
new Thread(()->{
for(int j=0;j<100;j++){
//这是CAS封装类中的操作,这个操作是值增加1
couter.incrementAndGet();
}
}).start();
}
//等待所有的线程完成
Thread.sleep(2000);
//打印最终计数器的值
System.out.println("最后计算的值为"+counter.get());
}
}
八股-解决线程死锁的办法
- 资源有序分配法
资源有序分配法通过为所有资源统一编号,并规定所有线程申请资源的顺序必须是按照资源的编号顺序(升序或降序)进行的。这样可以保证系统不出现死锁,因为它破坏了循环等待的条件。例如,如果系统中有两个资源R1和R2,所有线程都必须先申请R1再申请R2,这样就避免了循环等待的情况。
- 银行家算法
银行家算法是一种避免死锁的著名算法,主要用于操作系统中避免进程死锁。它借用了银行家借贷资金时,总要保证银行资金的安全性的思想。银行家算法在分配资源时,会检查资源分配的安全性,即判断当前状态下,是否存在一个安全序列,使得每个线程都能按序列顺序完成执行并释放资源,同时不会导致其他线程等待已分配的资源。
- 锁排序
锁排序是一种通过排序加锁操作来避免死锁的技术。在实践中,它是最有效且最常用的死锁阻止技术之一。锁排序要求所有需要加锁的代码都按照一个统一的固定顺序加锁,这样线程就只能向前单向等待锁释放,而无法形成一个环路,从而避免了死锁。例如,在数据库操作中,可以对数据库更新语句进行排序来阻止在数据库层面发生的死锁。
- 一次性请求所有资源
在程序设计中,可以尝试让线程在开始时一次性请求所需要的所有资源。如果线程无法一次性获得所有资源,则释放已获得的资源并重新请求。这种方法可以破坏持有并等待的条件,因为线程要么一开始就获得所有需要的资源并继续执行,要么从一开始就无法获得所有资源而等待或终止。
- 设置超时时间
在使用锁时,可以为锁操作设置超时时间。如果线程在指定时间内无法获得锁,则释放已持有的资源并放弃当前操作或重试。这种方法可以避免线程因长时间等待锁而导致资源浪费或死锁。
- 使用死锁检测和恢复机制
系统可以定期检测资源分配图,判断是否存在循环等待链,从而检测死锁。一旦检测到死锁,系统可以采取恢复措施,如资源抢占(从某个线程中强制剥夺资源并分配给其他线程)、回滚(回滚部分或全部死锁进程的操作)或终止进程(直接终止部分或全部死锁进程)等。
八股-synchronized和ReentrantLock的区别
是否轻量级锁:synchronized底层会根据不同的情况在轻量级和重量级之间进行变化,具体的变化参考后边的八股synchronized底层逻辑,而ReentrantLock底层是进行自旋锁轻量级锁,如果自旋也无法获取锁,则照样会进入到阻塞状态由JVM线程调度器进行调度
是否可重入:两个都是可重入锁避免死锁
是否挂起等待:synchronized是挂起等待,ReentrantLock首先自旋,自旋失败后挂起等待
是否公平:synchronized是非公平的,而ReentrantLock可以通过参数选择是否使用公平,参数true指设定为公平锁
八股-synchronized的底层逻辑
每个对象从创建之后会关联一个Monitor对象,其中包含了锁计数器,指向拥有该线程的指针
synchronized中底层锁的变化状态:
首先如果是单个线程且线程之间不在同一时间获得该锁,使用意向锁,在对象头中保存对应的线程的ID,这样如果下一次线程访问就不需要进行CAS操作,当有第二个线程,进入要获取锁的时候,如果第一个线程仍然要保持这个锁,我们就会进入到轻量级锁的过程中,进行CAS操作,如果在同一时间多个线程进行获取到这个锁,如果需要等待就会进行锁膨胀,膨胀之后就会尝试获取锁的时候进入到阻塞状态。
八股-如何保证线程之间的顺序性
- 使用只有单个的线程的线程池任务之间可以保证线程的顺序性
public void test(){
ExecutorService executor=Executors.newSingleThreadExecutor();
executor.submit(()->System.out.println("1"));
executor.submit(()->System.out.println("2"));
executor.submit(()->System.out.println("3"));
executor.shutdown();
}
多个任务等待一个线程,任务从队列中逐渐出来,保证了任务的顺序性
- 使用线程安全的队列
使用LinkedBlockingQueue作为线程等待的等待队列,这样先进先出的顺序就保证了线程的顺序性
public void test(){
ThreadPoolExecutor executor=new ThreadPoolExecutor(
1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>()
);
executor.submit(()->System.out.println("1"));
executor.submit(()->System.out.println("2"));
executor.submit(()->System.out.println("3"));
executor.shutdown();
}
- 使用Future和Callable,用Future监听所有的任务当某个任务完成才能进行下一个任务
public void test(){
ExecutorService executor=Executors.newFixedThreadPool(1);
Future future1=executor.submit(()->System.out.println("1"));
future1.get();
Future future2=executor.submit(()->System.out.println("2"));
future2.get();
Future future3=executor.submit(()->System.oyt.println("3"));
future3.get();
executor.shutdown();
}
等待上一个任务完成才能够进行下一步的操作,从而保证了线程的顺序性
- 一个线程等待另一个线程通过synchronized和CountDownLatch保证线程之间的顺序性
//使用synchronized保证任务之间顺序性
public class test{
private final Object obj=new Object();
public void test1(){
synchronized(obj){
System.out.println("1");
}
}
public void test2(){
synchronized(obj){
System.out.println("2");
}
}
Thread t1=new Thread(()->test1());
Thread t2=new Thread(()=>test2());
t1.start();
t2.start();
}
线程安全类扩展
- 使用Colliction的静态方法中的synchronizedList方法给当前给定的集合参数加锁
public void test(){
List<Integer> list=new List<>();
List<Integer> synlist=Collictions.synchronizedList(list);
//这个时候多个线程操作该集合的时候就会加锁,其中的关键方法会在同步代码块中执行
Thread thread1=new Thread(()->{
for(int i=0;i<5;i++){
synchronized(synlist){
synlist.add("线程当前值"+i);
System.out.println(synlist.get(synlist.size()-1));
}
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
Thread thread2=new Thread(()->{
for(int i=0;i<5;i++){
synchronized(synlist){
synlist.add("当前线程"+i);
System.out.println(synlist.get(synlist.size()-1));
}
try{
Thread.sleep(150);
}catch(InterruptedExecption e){
e.printStackTrace();
}
}
});
//这个时候启动线程
thread1.start();
thread2.start();
//主线程等待两个线程结束再进行主线程的操作
try{
thread1.join();
thread2.join();
}catch(TnterruptedException e){
e.printStackTrace();
}
System.out.println(synlist);
}
- 使用CopyOnWriteArrayList他会再向集合添加数据的时候会复制出一个容器,所有的写操作会先添加到这个容器之中进行,实现逻辑上的读写分离,达到高效率的多线程
public void test(){
List<Integer> list=new CopyOnWriteArrayList<>();
//创建两个线程一个进行写操作,一个进行读操作
Thread thread1=new Thread(()->{
for(int i=0;i<5;i++){
list.add(i);
System.out.println(i);
}
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
});
//创建线程进行写操作
Thread thread2=new Thread(()->{
for(int i=0;i<5;i++){
System.out.println(i);
}
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
});
}
对于这个集合来说,写和读是同时进行的,这个集合也维护了线程安全性
- 使用ConurrentHashMap,他实现HashTable做了一些优化,读的操作不进行加锁,只对写线程进行加锁,size的属性更新使用CAS原理,避免重量级加锁,集合的扩容通知维护一个新数组和一个旧数组
JUC
- Callable 见上文
- ReentrantLock 见上文
- Atomic CAS原子类操作 见上文
- 线程池 见上文
- Semaphore信号量
Semaphore:我的理解,就是将PV操作封装起来,P操作对应的acquire方法,V操作是release方法
//记录Semaphore的demo
public void test(){
//将PV操作的初始化值设定为10,进行PV操作
Semaphore test=new Semaphore(10);
Runnable runnable=new Runnable(){
@Override
public void run(){
try{
System.out.println("申请资源");
test.acquire();
Thread.sleep(1000);
System.out.println("释放资源");
test.release();
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
}
- CountDownLatch(多线程等待机制)
通过CountDownLatch进行多个线程后等待结果
public void test(){
CountDownLatch countdownlatch=new CountDownLatch(10);
Runnable runnable=new Runnable(){
@Override
public void run(){
try{
Thread.sleep(1000);
System.out.println("一名选手回来了");
countDownLatch.countDown();
}catch(InterruptedException e){
throws new RuntimeException(e);
}
}
};
for(int i=0;i<10;i++){
new Thread(runnable).start();
}
countDownLatch.await();
System.out.println("结束了");
}