1.线程和进程的区别
进程是资源分配的最小单位,线程是资源调度的最小单位
一个进程有多个线程。各个线程都拥有自己的堆栈、计数器、局部变量并且共享进程的资源,提高了cpu的利用率。
2.创建线程的四种方式
1.继承于Thread类
/**
* 多线程的创建,方式一:继承于Thread类
* 1.创建一个继承于Thread类的子类
* 2.重写Thread类的run()方法
* 3.创建Thread类的子类对象
* 4.通过此对象调用start()
*/
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
//2.重写Thread类的run()方法
public void run(){
//遍历100以内的所有的偶数
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName() + "运行,i = " + i) ;
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt1 = new MyThread() ; // 实例化对象
MyThread mt2 = new MyThread() ; // 实例化对象
mt1.setName("线程A");
mt2.setName("线程B");
//4.通过此对象调用start()
mt1.start();
mt2.start();
}
}
2.实现Runnable接口
Runnable runnable=new Runnable() {
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println("子线程输出,i = " + i) ;
}
}
};
Thread t1=new Thread(runnable);
t1.start();
for (int i=0;i<10;i++){
System.out.println("主线程输出,i = " + i) ;
}
//也可以简化成
new Thread(() -> {
for (int i=0;i<10;i++) {
System.out.println("子线程输出,i = " + i);
}
}).start();
for (int i=0;i<10;i++){
System.out.println("主线程输出,i = " + i) ;
}
3.通过Callable和FutureTask创建线程,Callable用于产生结果,FutureTask用于获取结果
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
}
return "子线程的返回结果是:"+sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask task=new FutureTask(new MyCallable());
Thread t1=new Thread(task);
t1.start();
//get()方法等待任务的程序执行完毕后才会执行
System.out.println(task.get());
}
}
4.通过线程池创建
@Configuration
@EnableAsync
public class TaskExecutePool {
@Bean
public Executor myTaskAsyncPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); //
executor.setMaxPoolSize(20); //最大线程数(要大于等于核心线程数)
executor.setQueueCapacity(1000); //队列大小
executor.setKeepAliveSeconds(300); //线程最大空闲时间
executor.setThreadNamePrefix("async-Executor-");//线程前缀名称
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
}
3.线程的start()和run()方法的区别
线程的start()方法用于启动线程,run()方法用于执行线程代码,run()可以重复调用,start()只能调用一次。
调用start()方法无需等待run()方法执行完毕,可以直接继续执行其他代码,此时线程处于就绪状态并没有运行,通过此Thread类调用run方法来完成其运行状态,run方法结束后线程终止,执行其他线程。
通过start()方法来调用run()方法而不是直接调用run()方法是因为通过start()方法启动线程,使线程处于就绪状态,直接调用run()是此Thread普通方法的调用。
4.线程的状态
新建:创建一个线程对象
就绪:通过start()方法使该线程处于就绪状态,等待线程的调用
运行:拿到了cpu的使用权,线程要进入到运行状态必须是就绪状态
阻塞:处于运行状态中的线程因为某种原因暂时放弃cpu的使用权,停止执行,进行阻塞状态,直到进入就绪状态才会被再次调用进入运行状态
阻塞分成三种等待阻塞、同步阻塞、其他阻塞
等待阻塞:运行中的线程执行wait()方法,jvm将线程放入等待队列(waitting queue)中,使线程进入等待阻塞状态
同步阻塞:线程在获取synchronized 同步锁失败,jvm将线程放入锁池(lock pool)中,线程会进入同步阻塞状态
其他阻塞:调用线程的sleep()或者join()方法或发出了I/O请求时线程进入到阻塞状态,当 sleep()状态超时、join()等待线程终止或者超 时、或者 I/O 处理完毕时,线程重新转入就绪状态。
结束:线程run()、main()方法执行结束,或者因异常退出了run()方法
5.线程同步
1.同步代码块
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized (同步锁对象){
操作共享资源的代码(核心代码 )
}
锁对象要求:对于当前同时执行的线程来说是同一个对象即可。对于实例方法建议使用this作为锁对象,对于静态方法建议使用字节码(类名.class)对象作为锁对象。
2.同步方法
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步方法底层是有隐式锁对象,锁的范围是整个方法代码。如果方法是实例方法,同步方法默认用this作为锁的对象。如果方法是静态方法,同步方法默认用类名.class作为锁的对象。
3.Lock锁
//final修饰后锁对象唯一
private final Lock lock=new ReentrantLock();
//上锁
lock.lock();
//解锁
lock.unlock();
synchronized与Lock都可以解决线程安全问题,synchronized机制在执行完相应的同步代码以后,自动的释放同步监听器。Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
优先使用顺序:Lock→同步代码块(已经进入了方法体,分配了相应的资源)→ 同步方法(在方法体之外)
6.死锁的定义以及发生条件
死锁:两个或两个以上的线程抢占资源造成的互相等待
产生死锁的四个条件:
1.互斥条件:某段时间内资源已被一个进程占用,如果还有其他进程请求该资源,只能等待占用该资源的进程用完释放
2.请求和保持条件:进程在请求新资源的时候保持对已有资源的占有
3.不剥夺条件:进程已获得的资源在未使用完前不会被剥夺,使用完后释放
4.环路等待条件:发生死锁时必然存在一个进程
7. Java 中用到的线程调度算法是什么?
有两种调度模型:分时调度模型和抢占式(java默认使用)调度模型。
分时调度模型: 平均分配每个线程占用的 CPU 的时间片
抢占式调度模型: 让优先级高的线程占用CPU,如果线程优先级相同,那么就随机选择一个线程
8.notify()和 notifyAll()有什么区别
当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall,使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。
9.wait 和 sleep 方法的区别
sleep()没有释放锁,让线程休眠指定的时间
wait方法释放了锁,等待其他线程调用notify/notifyAll唤醒
10.进程之间的通信方式
1.管道:内核中维护的一块内存缓冲区
2.命名管道:可以在不存在亲缘关系的进程中通信
3.信号:一种通知机制
4.消息队列:是一个消息链表,既可读消息,也可以写消息
5.共享内存:多个线程共享一片内存区域
6.内存映射:将磁盘文件映射到内存,修改内存就可以修改磁盘文件
7.信号量:设计了信号量以保证多进程并发访问共享变量的安全。信号量实则是一个计数器,拥有原子操作P和V。当信号量值小于等于0之后再进行P操作会把对应线程或进程阻塞。
8.Socket:一般用于不同主机之间的进程通信
11.乐观锁和悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的
12.Synchronize与ReentrantLock区别
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;