1.进程和线程的区别
进程:正在运行的程序,是操作系统分配资源的最小单元
线程:进程是由多个线程组成,是操作系统调度cpu最小单元,每个线程可以单独执行
线程更加轻量级,操作系统创建和销毁的时间和资源更少,同一个进程的线程可以共享内存空间,通信更加容易
进程更加重量级,操作系统创建和销毁的时间和资源更多,进程之间的通信相对复杂
2.为什么要使用多线程,优点和缺点
多线程是同时执行多条程序指令,且互相不影响
优点:
充分使用cpu的资源
将占据时间长的任务放到后台去处理
服务器同时处理多个用户,互不影响
通过设置优先级,优化性能
缺点:
占用内存大
多线程需要协调和管理
需要解决共享资源的抢占问题
3.并行和并发,同步和异步
并发:
同时执行多个任务,一个CPU内核会在多个线程间来回切换执行程序指令,不是真正同时执行
并行:
同时执行多个任务,多个CPU内核,一个内核执行一个线程,线程中指令是同时执行的
同步:
多个指令是排队执行的,效率比较低
异步:
多个指令同时执行(借助线程),效率比较高
4.线程实现的方法
java的实现有4种
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口
-
通过线程池创建
Thread类和Runnable类
thread类是继承的方式 不强制重写run方法
Runnable接口是实现的方式 强制重写run方法
callable接口是有返回值 前两种没有返回值
/**
* 自定义线程
*/
public class MyThread extends Thread{
//重写run方法
@Override
public void run() {
//Thread.currentThread() 当前线程
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "执行了" + i);
}
}
public static void main(String[] args) {
//创建线程,启动线程
//启动线程使用start,为什么不使用run? 使用start才能创建新的线程执行,使用run方法就是在主线程中执行
//多线程是如何执行???CPU的调度是抢占式,每个线程去抢cpu资源,执行过程可以被其它线程抢过去
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread();
thread.start();
}
//主方法中执行的线程是main
System.out.println(Thread.currentThread().getName() + "执行了");
}
}
/**
* 自定义Runnable对象
* 更加灵活,继承类后不能继承其它类,实现接口没有任何限制
* 语法更严格,必须重写run方法,继承类后不强制要求
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "执行了" + i);
}
}
public static void main(String[] args) {
// //创建Thread对象,同时传入Runnable对象
// Thread thread = new Thread(new MyRunnable());
// //启动线程
// thread.start();
//使用lambda表达式实现Runnable
for(int i = 0; i < 10;i++){
new Thread(() -> {
for (int j = 0; j < 100; j++) {
System.out.println(Thread.currentThread().getName() + "执行了" + j);
}
}).start();
}
}
}
/**
* 自定义Callable对象
*/
public class MyCallable implements Callable<Long> {
@Override
public Long call() throws Exception {
long sum = 0;
//长时间运算
for (int i = 0; i < 100000000L; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName()+"运算完毕!");
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建FutureTask对象,传入Callable对象
FutureTask<Long> futureTask = new FutureTask<>(new MyCallable());
//创建Thread对象,传入FutureTask对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
//获得运算结果
System.out.println("结果是:" + futureTask.get());
}
}
5.启动程序是start还是run
使用start才能创建新的线程执行run方法,使用run方法就是在主线程中执行
start是不能执行多次,一个线程对象只能调用一次
多线程的调用是cpu抢占式,每个线程去抢cpu资源,执行run过程可能被其他线程抢过去
6.sleep和wait区别
都可以让线程进入阻塞状态
区别:
-
调用对象不同:sleep是线程调用的,wait是锁对象(Object)调用的
-
释放锁不同:sleep不会让线程释放锁,wait会让线程释放锁
-
唤醒机制不同:sleep只能等睡眠时间结束,wait可以等时间结束也可以通过notify方法唤醒
7.多线程的执行是什么顺序
CPU的线程调度是抢占式,每个线程去抢cpu资源,执行run过程可以被其它线程抢过去
设置优先级,从低到高,1到10,优先级高的线程抢占cpu几率更高
8.线程的生命周期
线程生命周期:
-
新建
-
就绪
-
运行
-
阻塞
-
死亡
9.双检锁单例模式
public class MyTest {
private static MyTest myTest = null;
private MyTest() {
}
public static MyTest getTest() {
if (myTest == null) {
synchronized (MyTest.class) {
if (myTest == null) {
myTest = new MyTest;
}
}
}
return myTest;
}
}
10.并发编程三大特性,解释
原子性:程序指令的完整执行,要么全部执行,要么全部不执行
原因:多线程的调度是抢占式的,无法保证程序代码能过完整执行,会出现数据不一致的情况
可见性:对于共享资源,线程修改之后,其他线程要可以查看被修改后的状态
原因:每个cpu内核中都缓存有来自主内存的共享数据,一个内核中线程的修改,另一个内核不能及时修改
有序性:程序指令按照编码顺序执行
原因:jvm会对程序指令进行优化,可能会导致程序指令排序
11.为什么会出现线程安全问题,解决的方式有哪些
多线程的调度是抢占式的,无法保证程序代码的完整执行,则会出现数据不一致的问题
解决办法:通过上锁机制
1.synchronized关键字
2.reentrantLock类
12.synchronized做了哪些优化
jdk1.6之后
锁消除:jvm发现同步方法或同步块没有线程竞争的资源,会消除锁
锁膨胀:jvm发现循环中使用锁,会优化将锁放到循环体外
锁升级:锁状态:
-
无锁:没有竞争不能加锁
-
偏向锁:只有一个线程使用锁,锁会在对象头中记录线程的id,如果是这个线程直接放行
-
轻量锁:出现少量竞争情况下,CAS乐观锁机制进行调度,不会出现上下文切换,自旋等待(消耗cpu)
-
重量级锁:出现大量竞争情况下,会转化为重量级锁(互斥锁),线程出现上下文切换。
上锁的过程叫锁升级:无锁 -->偏向锁-->轻量锁-->重量锁 不能降级
13.synchronized的原理
jvm会启动Monitor监视器监控上锁的代码,线程进入后,监视器中计数器+1,其他线程进入时,监视器计数器不为0,就不运行其他线程进入,线程执行完代码后,计数器-1,监视器再让其他线程进入
14.synchronized和ReentrantLock有什么区别
-
上锁机制不同:synchronized是自动上锁和释放锁,ReentrantLock需要代码手动上锁和释放锁
-
锁的类型不同:synchronized是非公平锁,ReentrantLock在构造方法中设置公平锁或非公平锁
-
性能不同:ReentrantLock > synchronized
-
功能不同:ReentrantLock的功能远大于synchronized
15.什么是重入锁、公平锁非公平锁
ReentrantLock是重入锁(递归锁)
重入锁:发生方法递归情况下,持有锁的线程,可以重新持有该锁
公平锁:会维护等待线程的队列,锁释放后,优先让等待时间最长的线程先拿锁,降低线程的饥饿,降低程序的效率
非公平锁:所有线程都会去抢锁,谁抢到谁执行,有的线程会一直饥饿,效率高
public class LockDemo{
//创建重入锁
private ReentrantLock lock = new ReentrantLock();
public void testLock(){
//上锁
lock.lock();
try {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}finally {
//释放锁
lock.unlock();
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
lockDemo.testLock();
}).start();
}
}
}
16.为什么会出现可见性问题,如何解决
每一个cpu内核都有缓存主内存中的共享资源,在一个cpu内核中线程修改后,不能第一时间修改其他内核中缓存的共享资源
解决办法:volatile关键字修饰
被修饰的变量只保存在内存中,所有线程都直接读写主内存,避免了可见性问题
17.volatile关键字有什么作用,和synchronized的区别
volatile关键字的作用:修饰变量,保证变量的可见性
被修饰的变量只存储在主内存中,所有线程都读写主内存,避免了可见性问题
区别:
-
synchronized可以实现原子性,可见性,有序性,volatile实现可见性和有序性
-
synchronized更加重量级,消耗更多资源,volatile更加轻量级
-
synchronized用在方法或代码块上,volatile只能用于变量
18. 原子类起到什么作用,实现的原理是什么
多线程环境下,对变量的操作完整执行,解决线程安全问题
原理:使用乐观锁实现
乐观锁机制CAS:
对变量进行修改时,先读取变量的原始值,要修改时,在读取变量的当前值,如果两值相同,则用修改后的新值覆盖原始值,如果不同,就放弃修改
带来了新的ABA问题:原始值为A,线程1修改为B,线程2修改为A,这个过程中两值相同,让线程误以为没有并发问题出现
解决ABA:引入版本号机制,给变量加版本号,每次修改版本号加1,比较两值是否相同还要比较版本号是否改变
乐观锁:认为竞争较少,不给代码上锁,线程不会出现上下文切换,效率高
悲观锁:认为竞争较多,会给代码上锁,线程会出现上下文切换,效率低
19.线程的等待和通知通过哪些方法实现,分别介绍一下
在线程中使用this.wait(),或者 类名.calss.wait(),实现线程等待
线程中使用notify()来随机通知一个线程唤醒,notifyAll()唤醒所有等待该锁的线程
题目:
等待和通知案例
public class WaitNotifyDemo {
public synchronized static void testWait(){
try {
System.out.println(Thread.currentThread().getName() +"开始等待");
//让线程等待
WaitNotifyDemo.class.wait();
System.out.println(Thread.currentThread().getName() +"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// WaitNotifyDemo demo = new WaitNotifyDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
WaitNotifyDemo.testWait();
}).start();
}
//过5s主线程通知子线程执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知需要上上锁的代码内执行
synchronized (WaitNotifyDemo.class) {
WaitNotifyDemo.class.notifyAll();
}
}
}
线程交替输出: A线程循环输出A,B线程循环输出B,要求两个线程交替输出ABABAB.....
public class ThreadPrintDemo {
public static void main(String[] args) {
new Thread(() -> {
synchronized (ThreadPrintDemo.class) {
for (int i = 0; i < 10; i++) {
System.out.println("A");
try {
//通知对方线程执行
ThreadPrintDemo.class.notify();
//当前线程等待
ThreadPrintDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(() -> {
synchronized (ThreadPrintDemo.class) {
for (int i = 0; i < 10; i++) {
System.out.println("B");
try {
//通知对方线程执行
ThreadPrintDemo.class.notify();
//当前线程等待
ThreadPrintDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
try {
Thread.sleep(500);
synchronized (ThreadPrintDemo.class){
ThreadPrintDemo.class.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
20.介绍一下生产者消费者模式
多线程环境下,有的线程用于生产数据属于生产者,有的线程用于使用数据就消费者
存在问题:
-
耦合性高,生产者和消费者直接交互,互相影响,不利于代码维护
-
速度不一致,可能会发生消费者会等待,生产者资源浪费
实现办法:
-
需要定义一个缓冲区,缓冲区有临界值
-
生产者将数据存入缓冲区,缓冲区满了,生产者等待,通知消费者消费
-
消费者从缓冲区取出数据,缓冲区空了,消费者等待,通知生产者生产
21.介绍下阻塞队列是什么,有什么作用
BlockingQueue是一系列特殊的集合,这种集合会有临界值,以临界值为基准,自动让线程等待,也会自动唤醒线程
实现类:
ArrayBlockingQueue 数组结构的阻塞队列
LinkedBlockingQueue 链表结构的阻塞队列
用法:
put(T) 尾插法,达到临界值自动阻塞线程,小于临界值自动唤醒线程
T take() 头删法,如果空了自动阻塞线程,非空后会自动唤醒线程
int size() 数据个数
22.介绍下并发包中的常用类
CountDownLatch 类
作用:一个或多个线程等待其他线程工作执行完再执行自己的任务
方法:await() 让当前线程等待 countDown() 倒数一次,次数-1,次数为0 自动唤醒等待的线程
int getCount() 获得当前倒数次数
作用:一个或多个线程等待其它线程工作执行完再执行自己的任务
创建:
new CountDownLatch(倒数次数)
主要方法:
方法 | 说明 |
---|---|
await() | 让当前线程等待 |
countDown() | 倒数一次,次数-1,当次数为0,自动唤醒等待的线程 |
int getCount() | 获得当前倒数次数 |
PS: CountDownLatch对象只能使用一次
/**
* 倒数案例
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
//创建倒数对象
CountDownLatch countDownLatch = new CountDownLatch(3);
//创建三个线程,分别倒数一次
Thread thread = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把饭吃了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//倒数一次
countDownLatch.countDown();
});
Thread thread2 = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把垃圾倒了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
Thread thread3 = new Thread(()->{
System.out.println(Thread.currentThread().getName() + "把女朋友哄了,准备好了!!" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
thread.start();
thread2.start();
thread3.start();
try {
System.out.println("等等,我去找我兄弟去!!!");
//阻塞当前线程
countDownLatch.await();
System.out.println("我兄弟都来了!!!上啊!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Semaphore类
作用:信号量,可以用于控制执行任务的数量,主要用于限流(限制并发量)
用法:accquired() 请求信号量,信号量-1,到0时阻塞
release() 释放信号量,信号量+1,会唤醒一个阻塞的线程
创建方法:
new Semaphore(信号量大小)
new Semaphore(信号量大小,是否公平锁)
用法
方法 | 说明 |
---|---|
void accquired() | 请求信号量,信号量会减1,为0时就会阻塞 |
void release() | 释放信号量,信号量会加1,会唤醒一个阻塞的线程 |
限流案例
/**
* 信号量案例
*/
public class SemaphoneDemo {
public static void main(String[] args) {
//创建信号量对象
Semaphore semaphore = new Semaphore(10);
//限制只有10个线程能执行
for (int i = 0; i < 100; i++) {
final int n = i;
new Thread(() -> {
//消耗一个信号量
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "消耗了信号量" + n);
}).start();
}
try {
Thread.sleep(3000);
//释放信号量
semaphore.release(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CyclicBarrier类
循环栏栅,让多个线程等待一个线程,线程准备好后一起执行,类似CountDownLatch,区别是:可以重复使用
/**
* 循环栏栅
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
//创建栏栅对象
CyclicBarrier barrier = new CyclicBarrier(3,() -> {
System.out.println("发令枪响了!~!!");
});
// for (int j = 0; j < 2; j++) {
// //重置数量
// barrier.reset();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "准备好了!!");
try {
Thread.sleep(2000);
//开始等待, parties减1,到0就全部唤醒执行
barrier.await();
System.out.println(Thread.currentThread().getName() + "冲啊!!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
// }
}
}
23.介绍一下AQS
全称:AbstractQueuedSynchronizer 抽象队列同步器
大量并发包中的工具类都使用了AQS,基于AQS实现工具类的开发
AQS是一个抽象类,用于开发并发编程的类,包含:
-
成员变量
private volatile int state; //同步器状态
对于CountDownLatch来说,是倒数数量
对于Semaphore来说,是信号量数
对于ReentrantLock来说,是上锁状态
-
双向链表
保存处于等待状态的线程
对于Semaphore和ReentrantLock来说,可以保存公平锁的等待顺序
24.线程池有什么作用
作用:线程属于重要的系统资源,创建线程会消耗大量的服务器资源,线程池能够对线程资源进行回收,重复使用,以降低对服务器资源的消耗。
相关的API:
线程池顶层的接口: Executor
主要方法: execute(Runnable) 启动线程执行任务,执行完回收线程
子接口:ExecutorService
主要方法: shutdown() 停止线程池,会让其中线程继续把任务做完
shutdownNow() 停止线程池,中断正在执行的线程
实现类:ThreadPoolExecutor 具体线程池的实现类
工具类:Executors 用于快速创建线程池
25.创建线程池的几种方式
使用Executors工具类创建
-
newCachedThreadPool() 获取长度不限的线程池
-
newFixedThreadPool(数量) 获取长度限制的线程池
-
newSingleThreadExecutor() 获取长度单一的线程池
-
newScheduledThreadPool(数量) 获得调度的线程池
/**
* 线程池案例
*/
public class ThreadPoolDemo {
//不使用线程池,会造成大量线程资源的浪费
public static void testNonThreadPool(){
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() +" 执行了");
}).start();
}
}
//长度不限的线程池
public static void testCachedThreadPool(){
//创建长度不限的线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
//使用线程池启动线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +" 执行了");
});
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//关闭线程池
threadPool.shutdown();
}
//长度固定的线程池
public static void testFixedThreadPool(){
//创建长度不限的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
//使用线程池启动线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +" 执行了");
});
}
//关闭线程池
threadPool.shutdown();
}
//单一线程的线程池
public static void testSingleThreadPool(){
//创建长度单一的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
//使用线程池启动线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +" 执行了");
});
}
//关闭线程池
threadPool.shutdown();
}
//可调度的线程池
public static void testScheduledThreadPool(){
//创建长度单一的线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
//执行调度任务,参数1 任务 参数2 初始延迟 参数3 周期 参数4 时间单位
threadPool.scheduleAtFixedRate(() -> {
System.out.println("Hello!! " + LocalDateTime.now());
},2,1, TimeUnit.SECONDS);
//关闭线程池
// threadPool.shutdown();
}
public static void main(String[] args) {
// testNonThreadPool();
// testCachedThreadPool();
// testFixedThreadPool();
// testSingleThreadPool();
testScheduledThreadPool();
}
}
手动创建
使用ThreadPoolExecutor类创建
26.手动创建线程池
使用TreadPoolExecutor类创建,构造方法的参数如下
-
核心线程数,线程池中初始的线程数量
-
最大线程数,线程数最大值
-
存活时间,运行线程空闲的时间
-
时间单位
-
工作队列(阻塞队列),阻塞队列用于保存执行任务Runnable
-
线程工厂,用于创建线程
-
拒绝策略,处理暂时执行不了的任务
//自定义的线程池
public static void testThreadPoolExecutor(){
//获得内核数
int cpuNum = Runtime.getRuntime().availableProcessors() * 2;
//手动创建线程池
ExecutorService threadPool = new ThreadPoolExecutor(cpuNum,cpuNum,0,TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
for (int i = 0; i < 1000; i++) {
//使用线程池启动线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +" 执行了");
});
}
//关闭线程池
threadPool.shutdown();
}
27. 线程池参数怎么配置
-
核心线程数,要考虑当前任务情况:是计算密集型(大量使用CPU)还是IO密集型(大量进行数据IO)
计算密集性需要充分利用CPU内核,线程数量=CPU内核,获得方法:Runtime.getRuntime().availableProcessors();
IO密集型,耗时的IO操作会造成线程长时间阻塞,需要更多的线程,线程数量=CPU内核 * n (n >=2 n的大小取决于IO操作耗时情况和请求总数) 具体通过压力测试
-
最大线程数,可以设置和核心线程数一样,就避免频繁创建线程和销毁线程的消耗
-
存活时间,如果核心线程数和最大线程数一样,可以设置为0,如果不一样,尽量设置长一点,避免频繁创建和销毁
-
阻塞队列可以使用LinkedBlockingQueue,频繁添加和删除任务的效率高
28.线程池的原理
-
执行execute方法传入Runnable对象
-
判断当前线程有没有达到核心线程数
-
没有就添加核心线程,有再判断是否达到最大线程数
-
达到最大值就执行拒绝策略
-
添加线程时,创建Worker工作线程,启动工作线程
-
执行runWorker方法,会循环调用getTask()从工作队列取任务
-
取到任务就执行,如果任务取空,线程就被阻塞,直到有新任务
29.为什么synchronized会降低性能
synchronized属于互斥锁,一个线程持有锁时,会阻塞其它线程
线程的两个状态:
-
用户态
JVM能够管理的状态
-
内核态
JVM不能管理,由操作系统管理
上下文切换
线程在抢占资源时,发现资源上锁,线程从用户态转为内核态进行等待,线程获得锁,重新执行前会从内核态转换为用户,转换过程会降低性能,
切换的过程中需要保存或读取程序计数器的代码行数和寄存器的数据,比较消耗时间