线程的生命周期中,等待是主动的,阻塞是被动的
锁对象
创建锁对象,锁对象同一时间只允许一个线程进入
//创建锁对象
Lock lock=new ReentrantLock(true);//创建可重入锁
可重入锁:在嵌套代码块中,锁对象一样就可以直接进入执行
公平锁:保证线程获取锁的顺序与线程请求锁的顺序一致,即按照先来先服务的原则。
非公平锁:线程获取锁的顺序与线程请求锁的顺序无关,允许插队操作。
默认情况下的锁都是非公平锁,传入一个参数为true即为公平锁
加锁 lock
解锁 unlock
有加锁就要有解锁,否则会造成死锁
尝试加锁 tryLock
尝试加锁,如果加锁成功会返回true,失败返回false
public void method(){
//lock.lock();//加锁
if (lock.tryLock()){//尝试加锁:加锁成功返回true,失败false
System.out.println(Thread.currentThread().getName()+"加锁成功");
System.out.println(Thread.currentThread().getName()+"进入方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"结束方法");
lock.unlock();//解锁,忘记会发生死锁状态
}else {
System.out.println(Thread.currentThread().getName()+"加锁未成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method();
}
死锁(Deadlock)指的是在多线程编程中,两个或多个线程互相持有对方所需的资源,并且由于无法获得所需的资源而陷入无限等待的状态,导致程序无法继续执行下去。
发生死锁的必要条件有四个,分别是:
-
互斥条件(Mutual Exclusion):至少有一个资源每次只能被一个线程占用。
-
请求与保持条件(Hold and Wait):线程在持有资源的同时还可以请求其他资源。
-
不剥夺条件(No Preemption):线程不能被强制剥夺已经获得的资源,只能在自愿释放资源后才能由其他线程获取。
-
循环等待条件(Circular Wait):存在一种等待循环,即线程1等待线程2持有的资源,线程2等待线程3持有的资源,...,线程N等待线程1持有的资源。
当以上四个条件同时满足时,就可能发生死锁。一旦发生死锁,线程无法继续执行下去,程序会陷入无法解开的僵局,需要通过外部干预来解决。
为了避免死锁的发生,可以采取以下措施:
-
破坏互斥条件:例如,对于某些资源可以采用共享的方式,允许多个线程同时访问。
-
破坏请求与保持条件:一次性申请所有需要的资源,而不是一个一个地申请。
-
破坏不剥夺条件:允许线程在持有资源的同时,根据需要剥夺其他线程的资源。
-
破坏循环等待条件:通过定义资源的顺序,要求线程按照顺序申请资源,避免循环等待的发生。
以上措施可以在设计和实现多线程程序时考虑,来减少死锁的概率和影响。此外,通过合理的资源管理和使用,以及良好的编码规范,也可以减少死锁的发生。
ReentrantReadWriteLock
public static ReentrantReadWriteLock rrwl=new ReentrantReadWriteLock();
ReentrantReadWriteLock不是一个锁,是一个锁容器,里面有一个读锁(共享锁)和一个写锁(独占锁);原本有读锁时可以再加一个读锁,但写锁只能在没有任何线程占用时才能添加。
读锁 readLock
读锁常用于读多写少的场景,例如缓存系统、数据库查询等。多个线程可以同时获取读锁,同时读取共享资源,互不干扰。这样可以提高并发性和读取的效率。
public int get(int index){
Lock readLock = rwLock.readLock();
readLock.lock();
System.out.println(Thread.currentThread().getName()+"运行Get开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(index >= size){
throw new IndexOutOfBoundsException("index is"+index);
}
System.out.println(Thread.currentThread().getName()+"运行Get结束");
readLock.unlock();
return values[index];
}
写锁 writeLock
写锁常用于写多读少的场景,例如数据更新、写入操作等。在写锁加锁期间,其他线程无法获取读锁或写锁,确保了对共享资源的一致性和完整性。写锁是排他的,只允许一个线程进行写入操作。
public boolean add(int item){
Lock writeLock = rwLock.writeLock();
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"运行Add开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(size>values.length-1){
return false;
}
values[size++]=item;
System.out.println(Thread.currentThread().getName()+"运行Add结束");
writeLock.unlock();
return true;
}
线程通讯
线程通讯通过锁对象实现;
如何的对象都可以是锁对象,锁对象的方法在Object类中定义,但只有对象是锁对象的时候才能调用锁对象的方法,否则会报状态异常。
wait 等待
让执行到该行代码的线程进入等待状态(等待池)
notify 唤醒
唤醒一条被该锁对象wait的线程
当多个线程调用了wait方法进入等待状态时,一个线程调用notify方法会随机唤醒其中一个等待中的线程。也就是说,调用notify方法会唤醒一个线程,但是不确定唤醒的是哪个线程。
notifyAll 唤醒所有
唤醒所有被该锁对象wait的线程
public static final Object OBJECT=new Object();
public static void method(){
System.out.println(Thread.currentThread().getName()+"进入方法");
synchronized (OBJECT){//指定锁对象
OBJECT.notify();//唤醒一条被该锁对象wait的线程
//OBJECT.notifyAll();//唤醒全部被该锁对象wait的线程
System.out.println(Thread.currentThread().getName()+"进入同步代码块");
try {
try {
System.out.println(Thread.currentThread().getName()+"进入等待状态");
OBJECT.wait();//让执行到该行代码的线程进入等待状态(等待池)
//结束等待状态即进入就绪状态
System.out.println(Thread.currentThread().getName()+"重新运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"结束同步代码块");
OBJECT.notify();
}
}
wait和sleep的区别?
wait是Object类中定义的方法,可以由锁对象调用,让执行到该行代码的线程进入到等待状态;
sleep是Thread类中定义的静态方法,可以让执行到该行代码的线程进入到等待状态
区别:1.sleep需要传入一个毫秒数,达到时间后会自动唤醒
wait不能自动唤醒,必须调用notify或notifyAll方法唤醒
2.sleep方法保持锁状态进入等待状态
wait方法会解除锁状态,其他线程可以进入运行
当一个线程调用sleep方法时,它会暂停执行一段时间,但是它不会释放锁。换句话说,其他线程仍然无法进入该代码块,因为锁状态被保持。
而当一个线程调用wait方法时,它会释放锁,并进入等待状态。其他线程可以获取该锁并执行相应的代码块。当wait方法的线程被唤醒后,它会重新尝试获取锁,并继续执行代码。
线程池 Executors
完成线程的创建、管理和销毁工作
线程池是一种管理和维护线程的机制,它可以在应用程序中预先创建一组可重复使用的线程,并在需要时分配这些线程来执行任务。
线程池的主要作用是优化线程的创建和销毁开销,提高线程的重用率和执行效率。通过线程池,可以将任务提交给线程池,线程池会自动管理线程的生命周期,使得线程可以被重复利用,避免频繁地创建和销毁线程的开销。
创建线程池对象
线程池ThreadPoolExecutor的构造方法有7个参数依次是:
int corePoolSize 1.核心线程数 int maximumPoolSize 2.最大线程数 long keepAliveTime 3.持续存活时间 TimeUnit unit 4.时间单位 BlockingQueue<Runnable> workQueue 5.任务队列 ThreadFactory threadFactory 6.线程工厂(用于创建线程) RejectedExecutionHandler handler 7.拒绝执行的处理器(回绝策略)
ArrayBlockingQueue qu = new ArrayBlockingQueue(12);
//创建线程池对象
ThreadPoolExecutor tpe=new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS,
qu, Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
实现Runnable接口或者Callable接口都可以作为线程任务执行
execute 方法 执行 :
将Runnable的线程任务交给线程池执行
Runnable run =EasyExecutors::method;
//交给线程池执行
tpe.execute(run);
submit 方法 提交:
将实现Runnable接口或Callable接口的线程任务提交给线程池执行
Callable<String> call = EasyExecutors::methodCall;
Future<String> f = tpe.submit(call);//提交
//tpe.submit(run);
shutdown 方法 关闭:
//关闭线程池
tpe.shutdown();//结束,销毁
Callable接口
Callable接口定义了一个单一的方法call(),该方法在执行任务时可以返回结果。与Runnable的run()方法不同,call()方法可以返回一个泛型类型的结果,也可以抛出一个Exception。在执行Callable任务时,可以通过Future对象来接收任务的返回结果。
Future类
Future类是用来表示异步计算结果的,它提供了一系列的方法来获取、取消和监控异步任务的执行状态,使得多线程编程更加方便和灵活。
get 方法 获取 :会等待线程执行完毕
cancel 方法 取消 :
对于正在执行中的任务,cancel方法的mayInterruptIfRunning参数可设为true,这样可通过中断执行线程来中断任务。通过其他途径来中断Executor内的线程是不允许的,因为你不知道该线程正在执行哪个任务;而Future对象是Executor创建的,它会与Executor线程池互相配合来实现其任务中断策略。
Callable<String> call = EasyExecutors::methodCall;
Future<String> f = tpe.submit(call);//提交
System.out.println(f.get());//会等待线程执行完毕
f.cancel(true);//取消任务执行
线程池的4种回绝策略
1. AbortPolicy (默认)放弃该任务并会抛出一个异常RejectedExecutionException
2. CallerRunsPolicy 调用者执行,让传递任务的线程执行此任务
3. DiscardOldestPolicy 放弃队列中最早提交的任务,不会抛出异常
4. DiscardPolicy 直接放弃新的任务,不会抛出异常
线程池的工作原理:
任务放置在工作队列中
1. 先检查池中是否有空闲的线程,如果有就让该线程执行任务
2. 如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数:
没有达到核心线程数,创建新的线程执行任务,直到填满核心线程数;
如果已经达到,优先在队列中存储,直到队列填满;
3. 工作队列填满后,再添加新的任务,看是否达到最大线程数:
如果没有,创建新的线程执行任务,直到填满最大线程数;
已经填满最大线程数,队列也已经填满,没有空闲线程,就执行回绝策略
4. 线程池中的线程达到(超过)核心线程数,超出的数量在空闲时会根据存活时间进行销毁:
直到数量达到核心线程数,如果线程的数量少于核心线程数,不会消亡
Java中内置的线程池:
//Java中内置的线程池
//可以根据工作任务来创建线程,如果没有空闲的线程就创建新的线程 线程存活时间60s
Executors.newCachedThreadPool();
//设定最大线程数量的线程池
Executors.newFixedThreadPool(10);
//提供定时运行的处理方案
Executors.newScheduledThreadPool(10);
//创建一个具有单个线程的线程池,保障任务队列完全按照顺序执行
Executors.newSingleThreadExecutor();
在高并发高任务情况下,核心线程数怎么设置合适?
取决于以下几个因素:
-
系统资源:核心线程数应根据系统的处理能力来确定。如果系统的CPU、内存等资源充足,可以适当增加核心线程数来提高并发处理能力。
-
任务类型:不同类型的任务对线程的需求不同。如果任务是CPU密集型,即需要大量的计算资源,那么线程数可以设置得较少,以避免线程过多而导致资源竞争。如果任务是IO密集型,即需要等待IO操作的完成,那么线程数可以设置得较多,以充分利用CPU资源。
-
响应时间:线程池的核心目标是提供快速响应的服务。核心线程数的设置应能够确保任务能够在合理的时间内得到处理,而不是等待线程的创建和启动。
-
可用线程数:线程池的总线程数应根据系统的可用线程数来设置。可用线程数是指除了线程池的核心线程数之外,系统还能够分配给线程池的最大线程数。一般情况下,可用线程数可以设置为核心线程数的两倍或更多。
综合考虑以上因素,合适的核心线程数设置可以先根据系统资源和任务类型进行初步估算,然后进行性能测试和调优,根据实际情况进行调整。
枚举类
枚举类是一种特殊的类,它定义了一组常量作为其实例,并提供了一些功能扩展。
在Java中,我们可以使用关键字"enum"来定义枚举类。枚举类的实例通常用于表示一组相关的常量,例如颜色、星期几、季节等。
需要注意的是,枚举类的实例是唯一的,可以通过调用枚举常量的名称进行比较。例如,使用day == Day.MONDAY来比较两个枚举常量是否相等。
用关键字 enum 来创建枚举类,枚举类首行必须枚举所有实例
枚举类默认继承Enum,但不能写继承自Enum;
是可比较的:根据实例声明的顺序
枚举类是不可序列化的,也不能克隆
public enum EasyColor {
RED,YELLOW,GREEN,BLUE,PINK;
public void printColor(){
System.out.println(this.name());
System.out.println(this.ordinal());
}
}
class Test{
public static void main(String[] args) {
EasyColor.GREEN.printColor();
}
}