JUC并发编程
多线程
三种创建方式
- 继承Thread(Thread实现了Runnable接口)
- 实现Runnable(这种方式需要将该实现类作为参数调用Thread对象)
- 实现Callable
线程的状态
线程方法
yield()方法会重新让线程回到就绪状态,所以不一定会礼让成功
join()一定会礼让成功
线程状态
synchronized默认锁的对象是this
CopyOnWriteArrayList<?>()是线程安全的List
死锁的产生条件
什么时候会释放锁
由于等待一个锁定线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:
1、当前线程的同步方法、代码块执行结束的时候释放
2、当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
3、当前线程出现未处理的error或者exception导致异常结束的时候释放
4、调用obj.wait()会立即释放锁,当前线程暂停,释放锁,以便其他线程可以执行obj.notify(),但是notify()不会立刻立刻释放sycronized(obj)中的obj锁,必须要等notify()所在线程执行完synchronized(obj)块中的所有代码才会释放这把锁。而 yield(),sleep()不会释放锁。
除了以上情况外,只要持有锁的此案吃还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁:
1. 在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
2. 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
3. 在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃
并发三大特性
原子性、可见性、有序性
原子性
含义
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作。
重要
不采取任何的原子性保障措施的自增操作并不是原子性的。
如何保证原子性
- 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
- 通过 Lock 接口保障原子性。
- 通过 Atomic 类型保障原子性。
可见性
含义
当一个线程修改了共享变量的值,其他线程能够看到修改的值。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
volatile 变量和普通变量区别
普通变量与 volatile 变量的区别是 volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
如何保证可见性
- 通过 volatile 关键字标记内存屏障保证可见性。
- 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
- 通过 Lock 接口保障可见性。
- 通过 Atomic 类型保障可见性。
- 通过 final 关键字保障可见性
有序性
含义
即程序执行的顺序按照代码的先后顺序执行。
JVM 存在指令重排,所以存在有序性问题。
如何保证有序性
- 通过 synchronized关键字 定义同步代码块或者同步方法保障可见性。
- 通过 Lock接口 保障可见性
wait()和sleep()的区别
wait()
- 方法来自于Object
- 会释放锁
- 必须在同步代码块中使用
sleep()
- 方法来自于Thread
- 不会释放锁
- 可以在任意地方使用
Lock锁
由三个子类提供对象
重要方法
- lock.lock()
- trylock()
- unlock()
公平锁与非公平锁
Synchronized和Lock的区别
- Synchronized是java的内置关键字,Lock是java类
- Synchronized无法获取锁的状态,Lock可以
- Synchronized会自动释放锁,Lock不会
- Synchronized如果阻塞会一直等待,Lock则不会
- Synchronized锁是非公平锁,Lock可以直接设置
- Synchronized适合锁少量的代码同步问题,Lock适合大量
8锁问题
Synchronized修饰的方法锁的对象是方法的调用者
一个对象有一把锁
static的Synchronized表示锁的是类.Class
类.Class的锁和对象的锁不是同一把锁
Callable(泛型接口,泛型的类型为call方法的返回值)
可以有返回值
可以抛出异常
call()方法
有缓存,可能需要等待,get()可能会阻塞
JUC常用三大辅助类
CountDownLatch(减法计数)
//减法计数器 CountDownLatch countDownLatch = new CountDownLatch(8); countDownLatch.countDown();//每执行一次就减1 countDownLatch.await();//当减到0的时候就唤醒,然后向下执行
CyclicBarrier(加法计数)
public CyclicBarrier(int parties) public CyclicBarrier(int parties, Runnable barrierAction)
await()方法告知CyclicBarrier线程已经到达,然后阻塞
Semaphore
ReadWriteLock
已知所以实现类:ReentrantReadWriteLock
两个方法
- writeLock()
- readLock()
阻塞队列
![](/i/l/?n=22&i=blog/2884036/202207/2884036-20220705222515056-1730857290.png)
ArrayBlockingQueue
SynchronousQueue
不储存元素,put()一个元素之后就要take()出来,否则就会阻塞
线程池
Executos
Executors.newCachedThreadPool();//可升缩大小 Executors.newFixedThreadPool(5);//固定大小 Executors.newSingleThreadExecutor();//但个线程
三大方法
七大参数
(1)corePoolSize:线程池中的常驻核心线程数。
(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
- CPU密集型,获取电脑核数(Runtime.getRuntime().availableProcessors();)
- IO密集型,为耗时IO线程的两倍
(3)keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。
(4)unit:keepAliveTime的单位。
(5)workQueue:任务队列,被提交但尚未被执行的任务。
(6)threadFactory(Executors.defaultThreadFactory();):表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可。
(7)handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略。
流程分析
- 线程池中线程数小于corePoolSize时,新任务将创建一个新线程执行任务,不论此时线程池中存在空闲线程;
- 线程池中线程数达到corePoolSize时,新任务将被放入workQueue中,等待线程池中任务调度执行;
- 当workQueue已满,且maximumPoolSize>corePoolSize时,新任务会创建新线程执行任务;
- 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理;
- 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收该线程;
- 如果设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收;
一:corePoolSize 详细描述
(1)在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近视理解为今日当值线程。
(2)当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中。
二:最大线程数(maximumPoolSize):该参数定义了一个线程池中最多能容纳多少个线程。当一个任务提交到线程池中时,如果线程数量达到了核心线程数,并且任务队列已满,不能再向任务队列中添加任务时,这时会检查任务是否达到了最大线程数,如果未达到,则创建新线程,执行任务,否则,执行拒绝策略。可以通过源码来看一下。如下:可以看出,当调用submit(Runnable task)方法,将任务提交到线程池中时,会调用execute()方法去执行任务,在该方法内,会进行核心线程数,任务队列的判断,最后决定是执行或者是拒绝。总结起来就是:最大线程数参数,是在已经达到核心线程池参数,并且任务队列已经满的情况下,才去判断该参数。
三:keepAliveTime 详细描述
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze。
四:系统默认的拒绝策略有以下几种:
- AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
- DiscardPolicy:直接抛弃不处理。
- DiscardOldestPolicy:丢弃队列中最老的任务。
- CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理。
四大函数式接口
Function<T,R> 函数式接口: R apply(T t);
Predicate<T> 断定型接口:boolean test(T t); 有一个输入参数,返回值只能是 布尔值!
Consumer<T> 消费型接口:void accept(T t); 只有输入,没有返回值
Supplier<T> 供给型接口: T get(); 没有参数,只有返回值...
ForkJoin
工作窃取算法
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点:
充分利用线程进行并行计算,并减少了线程间的竞争。
工作窃取算法的缺点:
在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。
Fork/Join框架局限性:
对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程查找其他未被执行的任务,并开始执行这些未被执行的任务,通过这种方式,线程充分利用它们的运行时间来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性。
(1)任务只能使用Fork和Join操作来进行同步机制,如果使用了其他同步机制,则在同步操作时,工作线程就不能执行其他任务了。比如,在Fork/Join框架中,使任务进行了睡眠,那么,在睡眠期间内,正在执行这个任务的工作线程将不会执行其他任务了。
(2)在Fork/Join框架中,所拆分的任务不应该去执行IO操作,比如:读写数据文件。
(3)任务不能抛出检查异常,必须通过必要的代码来处理这些异常。
ForkJoinPool里面的方法
- public void execute(ForkJoinTask<?> task)
- public void execute(Runnable task)
- public <T> T invoke(ForkJoinTask<T> task)
- public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
- public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
- public <T> ForkJoinTask<T> submit(Callable<T> task)
- public <T> ForkJoinTask<T> submit(Runnable task, T result)
- public ForkJoinTask<?> submit(Runnable task)
异步回调
无返回值
有返回值
其父类的一部分方法
JMM
JMM内存模型
volatile
- 保证可见性
- 不保证原子性
- 禁止指令重排