文章目录
1 线程同步Synchronized 和 ReentrantLock用法
同步异步 :
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就 是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行 线程阻塞和唤醒的代价是比较高的.
synchronized
修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经 拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释
放为止。
public synchronized void mehthodA(){
//业务代码。。。。
}
public synchronized static void mehthodB(){
}
@Override
public void run() {
while(ticketNum>0){
synchronized (this){
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+"正在卖票,剩余"+(--ticketNum)+"张");
}
}
}
}
ReentrantLock
而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配
合try/finally语句块来完成。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,
ReentrantLock类提供了一些高级功能,
主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性
能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。
while(ticketNum>0){
try {
//加锁
reentrantLock.lock();
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+"正在卖票,剩余"+(--ticketNum)+"张");
}
} finally {
//解锁
reentrantLock.unlock();
}
}
2 sleep和wait的区别
sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线 程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一 个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法 (notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果 notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果),调用wait方法的 线程就会解除wait状态和程序可以再次得到锁后继续向下运行。
package com.aaa.mt.demo2;
/**
* @ fileName:SleepAndWaitDemo
* @ description: sleep和wait区别:
* 1,sleep 在不在同步块中都可以执行(不在同步块直接让出cpu,在同步块回不会释放锁) wait 必须在同步块执行
* 2,sleep 是Thread类中的静态方法 wait是Object类的方法
* 3,都在同步块中时,sleep不会释放锁 wait会释放锁产生阻塞直到有其他拿锁唤醒才具备拿锁资格,并不是立马拿到锁执行,而是等到t2执行完释放锁,再执行
* @ author:zhz
* @ createTime:2021/12/1 9:51
* @ version:1.0.0
*/
public class SleepAndWaitDemo {
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("浮世三千,吾爱有三");
try {
// Thread.sleep(3000);
lock.wait();//阻塞并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("日月与卿");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("日为朝 月为暮");
lock.notify();
try {
//测试是否唤醒t1后,t1立马拿到锁,不会立马拿到
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卿为朝朝暮暮");
}
}
});
t1.start();
t2.start();
}
}
3 死锁和如何防止死锁
死锁成因:
当前线程拥有其他线程需要的资源
当前线程等待其他线程已拥有的资源
都不放弃自己拥有的资源
线程1和线程2 资源A和资源B 线程1 拥有 资源A 线程2拥有 资源B
线程1想拿到资源B 线程2想拿到资源A
死锁实例:
package com.aaa.mt.demo3;
/**
* @ fileName:DeadLock
* @ description:
* @ author:zhz
* @ createTime:2021/12/1 10:49
* @ version:1.0.0
*/
public class DeadLock implements Runnable{
private Father father =new Father();
private Son son = new Son();
//标识线程是否是父亲
private Boolean isFather=true;
//锁
private static Object lockA = new Object();
private static Object lockB = new Object();
@Override
public void run() {
//判断是否是父亲线程
if(isFather){//父亲线程操作
synchronized (lockA){
father.say();
try {
//让线程休眠给对象拿锁机会
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取儿子的锁
synchronized(lockB){
father.get();
}
}
}else {//儿子线程操作
synchronized (lockB){
son.say();
try {
//让线程休眠给对象拿锁机会
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取父亲的锁
synchronized(lockA){
son.get();
}
}
}
}
public static void main(String[] args) {
DeadLock deadLock1 =new DeadLock();
deadLock1.isFather=true;
DeadLock deadLock2 =new DeadLock();
deadLock2.isFather=false;
//启动线程
new Thread(deadLock1).start();
new Thread(deadLock2).start();
}
}
如何防止:
1,避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。例如上面的死锁程序,主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
2,具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
3,使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
4,死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。
4 线程池概念和作用
概念:
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
作用:
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系 统的稳定性,使用线程池可以进行统一的分配,调优和监控。
5 线程池4种常用方式
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
本质了解:
ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大线程池大小
long keepAliveTime,//线程池中超过corePoolSize数目的空闲线程最大存活时间
TimeUnit unit,//时间单位
BlockingQueue workQueue)//线程等待队列
corePoolSize,maximumPoolSize,workQueue之间关系。
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
newSingleThreadExecutor: 创建一个单线程的线程池。这个线程池只有一个线程工作。如果这个线程出现异常,会有一个新的线程来替代它。此线程保证所有的任务执行顺序是按照提交顺序执行。
corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;
适用场景:保证任务由一个线程串行执行
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
int index=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+index);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
executorService.shutdown();
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 线程池的大小一旦达到最大值就会保持不变,如果某个线程出现异常,那么会补充一个新的线程。
corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程;
workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;
FixedThreadPool的任务执行是无序的;
适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
int index=i;//内部类使用,在使用前不是定义
Thread.sleep(10);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+","+index);
}
});
}
executorService.shutdown();
newCachedThreadPool: 创建一个可缓存的线程池。如果线程池的大小超过处理任务所需要的线程数, 那么会回收部分空闲线程,当任务数 增加时,线程会智能添加新线程来处理任务。此线程不会对线程池的大小做限制,线程池大小完全依赖于操作系统(或 JVM)能够创建最大线程的大小。 (如果间隔时间长,下一个任务运行时,上一个任务已经完成,所以线程可以继续复用,如果间隔时间调短,那么部分线程将会使用新线程来运行。)
corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
keepAliveTime = 60s,线程空闲60s后自动结束。
workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;
适用场景:快速处理大量耗时较短的任务
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
int index=i;//内部类使用,在使用前不是定义
Thread.sleep(10);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+","+index);
}
});
}
executorService.shutdown();
newScheduledThreadPool: 创建一个无限大小的线程池。此线程池支持定时及周期性执行任务的需求。
适应场景:定时执行任务
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(3);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行");
}
},10, TimeUnit.SECONDS);
/*scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
}
},5,2,TimeUnit.SECONDS);*/
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println("执行1...");
}
},1,5,TimeUnit.SECONDS);
6 线程池submit和execute区别
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
package com.aaa.mt.demo5;
import java.util.concurrent.*;
/**
* @ fileName:MTPoolSubmitAndExecuteDemo
* @ description: submit和execute 区别:
* 1,submit即支持Runnable还支持Callable execute 只支持Runnable
* 2,submit 因为支持Callable,所以就可以支持获取返回值
* 3,submit 因为支持Callable,所以就可以支持获取异常处理
* @ author:zhz
* @ createTime:2021/12/1 11:47
* @ version:1.0.0
*/
public class MTPoolSubmitAndExecuteDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("submit支持Runnable");
}
});
Future<Object> future= executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println("submit支持Callable");
System.out.println(1/0);
return 1;
}
});
System.out.println(future.get());
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("submit支持Runnable");
}
});
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/75501.html