throw跟throws的区别
throw,作用于方法内,用于主动抛出异常
throws, 作用于方法声明上,声明该方法有可能会抛些某些异常
针对项目中,异常的处理方式,我们一般采用层层往上抛,最终通过异常处理机制统一处理(展示异常页面,或返回统一的json信息),自定义 异常一般继承RunntimeException,我们去看看Hibernate等框架,他们的异常体系都是最终继承自RunntimeException
创建线程的方式
我们常说的方式有以下三种:
继承Thread
实现Runable接口
实现Callable接口(可以获取线程执行之后的返回值)
但实际后两种,更准确的理解是创建了一个可执行的任务,要采用多线程的方式执行,
还需要通过创建Thread对象来执行,比如 new Thread(new Runnable(){}).start();这样的方式来执行。
在实际开发中,我们通常采用线程池的方式来完成Thread的创建,更好管理线程资源。
案例:如何正确启动线程
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running.....");
}
}
public static void main(String[] args){
MyThread thread = new MyThread();
//正确启动线程的方式
//thread.run();//调用方法并非开启新线程
thread.start();
}
案例:实现runnable只是创建了一个可执行任务,并不是一个线程
class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running....");
}
}
public static void main(String[] args){
MyTask task = new MyTask();
//task.start(); //并不能直接以线程的方式来启动
//它表达的是一个任务,需要启动一个线程来执行
new Thread(task).start();
}
案例三:runnable vs callable
class MyTask2 implements Callable{
@Override
public Boolean call() throws Exception {
return null;
}
}
明确一点:
本质上来说创建线程的方式就是继承Thread,就是线程池,内部也是创建好线程对象来执行任务
一个普通main方法的执行,是单线程模式还是多线程模式?为什么?
因为java有个重要的特性,叫垃圾自动回收机制,所以答案是多线程,这里面有两部分,主线程(用户线程),垃圾回收线程GC(守护线程)同时存在。
什么是死锁
死锁,哲学家问题。
转换到线程的场景,就是线程A持有独占锁资源a,并尝试去获取独占锁资源b
同时,线程B持有独占锁资源b,并尝试去获取独占锁资源a
这样线程A和线程B相互持有对方需要的锁,从而发生阻塞,最终变为死锁。
public class Deadlock {
private static final Object a = new Object();
private static final Object b = new Object();
public static void main(String[] args){
new Thread(new Task(true)).start();
new Thread(new Task(false)).start();
}
static class Task implements Runnable{
private boolean flag;
public Task(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (a){
System.out.println(Thread.currentThread().getName()+"->获取到a资源");
synchronized (b){
System.out.println(Thread.currentThread().getName()+"->获取到b资源");
}
}
}else{
synchronized (b){
System.out.println(Thread.currentThread().getName()+"->获取到b资源");
synchronized (a){
System.out.println(Thread.currentThread().getName()+"->获取到a资源");
}
}
}
}
}
}
//有可能会出现死锁,如果第一个线程已经走完,第二个线程才获取到执行权限,那么就不会出现死锁
2,如何防止死锁?(重点)
减少同步代码块嵌套操作
降低锁的使用粒度,不要几个功能共用一把锁
尽量采用tryLock(timeout)的方法,可以设置超时时间,这样超时之后,就可以主动退出,防止死锁(关键)
死锁满足的四个条件
互斥,不可剥夺,循环等待,请求和保持
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
Sleep和wait的区别
1,所属的类不同:
sleep方法是定义在Thread上
wait方法是定义在Object上
2,对于锁资源的处理方式不同
sleep不会释放锁
wait会释放锁
3,使用范围:
sleep可以使用在任何代码块
wait必须在同步方法或同步代码块执行
4,与wait配套使用的方法
void notify()
Wakes up a single thread that is waiting on this object’s monitor.
译:唤醒在此对象监视器上等待的单个线程
void notifyAll()
Wakes up all threads that are waiting on this object’s monitor.
译:唤醒在此对象监视器上等待的所有线程
void wait( )
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object.
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
生命周期
1,当线程调用wait()或者join时,线程都会进入到waiting状态,当调用notify或notifyAll时,或者join的线程执行结束后,会进入runnable状态
2,当线程调用sleep(time),或者wait(time)时,进入timed waiting状态,
最后,留下一个思考题,为什么wait要定义在Object中,而不定义在Thread中?
来解释下,我们回想下,在同步代码块中,我们说需要一个对象锁来实现多线程的互斥效果,也就是说,Java的锁是对象级别的,而不是线程级别的。
为什么wait必须写在同步代码块中?
原因是避免CPU切换到其他线程,而其他线程又提前执行了notify方法,那这样就达不到我们的预期(先wait再由其他线程来唤醒),所以需要一个同步锁来保护
JDK提供的线程池有哪些?实际开发我们该怎么使用?
1,JDK通过接口ExecutorService来表示线程池,通过工具类Executors来创建多种线程池对象
2,各种线程池的特点如下:
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
3,在实际开发中,我们是怎么使用的?(重点)
实际开发中,线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
实际开发中,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式
FixedThreadPool 和 SingleThreadPool,允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool,允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
所以,综上所述,我们都会采用底层的方式来创建线程池,大家自己查阅各种线程池的源代码就可以看到他们都是采用了同一个类来创建