一、线程与进程
一个程序的执行,首先把可执行文件放到内存,找到起始(main)的地址,逐步读出指令和数据,进行计算并写回到内存。
1)、什么是线程?
线程是一个动态概念,线程是CPU调度的基本单位,是可执行的计算单位。
线程的概念:共享空间,不共享计算。
2)、什么是进程?
一个程序放到内存里,就叫进程。
一个程序可以有多个进程,比如我们在电脑上,运行多个QQ程序。
进程是一个静态概念,进程是一个分配资源的基本单位。
二、线程切换(OS)
1)、 主要功能是,保存上下文。
比如:我们有两个线程,一个T1线程,一个T2线程,T1有三个命令,T2也有三个命令,这个时候去访问线程T1去访问执行到第二个命令了,然后要换成T2访问,我们需要吧T1已经访问过的数据,存进缓存中,当T2执行完后,从缓存中拿取T1,在执行T1时我们直接在第二个命令继续执行。
问题:
1.1)、是不是线程数量越多,执行效率就越高?
答:不是,资源都浪费在线程切换上了。比如:十万个活着的线程,来回切换,效率低。
1.2)、单核CPU多线程执行有没有意义?
答:有意义,比如:一个程序在访问是,因为网络原因,CPU不能一直傻傻的等待,可以吧资源让给别人继续访问
1.3)、工作线程数(线程池中线程数量)设置多少合适?
答:我们也可以用一个公式来计算,如下:、
W:50%、C:50%,Ucpu期望:50% (1+1)两个线程。
w/c这个是我们是没有办法知道的,主要还是要压测来测试,多少线程能够充分利用CPU。
三、启动线程的三种方式(实现多线程的四种方法)
1)、启动线程三种方式
1.1)、Thread方法
1.2)、Runnable方法
1.3)、可以使用Lambda表达式,或使用线程池Executors.newCachedThrad
2)、继承Thread方法实现多线程
重写Thread中的run方法,来实现多线程,这种方式我们一般是不用的,因为java类是单继承的,使用继承方式不利于扩展性的。
public class DemoThread extends Thread{
@Override
public void run() {
int i = 10;
for (int i1 = 0; i1 < i; i1++) {
try {
// 睡眠 一秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i的值" + i1 + "子线程名称1:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 创建线程类对象
DemoThread domeThread1 = new DemoThread();
// 启动线程
domeThread1.start();
int a = 10;
for (int i = 0; i < a; i++) {
System.out.println(">>>>>>>>a的值" + i + "主线程名称:"+ Thread.currentThread().getName());
}
}
}
3)、实现Runnabel接口实现多线程
重写Runnabel中的run方法,但是要启动线程,需要通过 Thread来启动Runnabel线程来,实现多线程。
注:Runnabel接口是没有返回值的。
public class DemoRunnable implements Runnable{
@Override
public void run() {
int i = 10;
for (int i1 = 0; i1 < i; i1++) {
System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
DemoRunnable domeRunnable1 = new DemoRunnable();
// 通过new Thread 来启动
Thread thread = new Thread(domeRunnable1);
// 启动线程
thread.start();
/**
* 另一种实现方式
*/
new Thread(new Runnable() {
@Override
public void run() {
int i = 10;
for (int i1 = 0; i1 < i; i1++) {
System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
}
}
}).start();
/**
* java8 写法
*/
new Thread(() -> {
int i = 10;
for (int i1 = 0; i1 < i; i1++) {
System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
}
}).start();
int a = 10;
for (int i = 0; i < a; i++) {
System.out.println(">>>>>>a的值" + a + "主线程名称" + Thread.currentThread().getName());
}
}
}
4)、实现Callable接口实现多线程
通过Callable实现,重写call方法,使用Callable配合Future一起使用,这我用的是FutureTask,它的父类实现了RunnableFuture接口,RunnableFuture接口继承了Runnable, Future<V>这连个,感兴趣的可以去看下源码。
启动线程,我们还是用 THread来启动。
注:Callable接口是有返回值的,它是一个泛型。
public class DemoCallable implements Callable<Integer> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程实现类
DemoCallable domeCallable1 = new DemoCallable();
// 创建FutureTask对象
FutureTask<Integer> t1 = new FutureTask<>(domeCallable1);
// 通过 Thread来启动
Thread thread = new Thread(t1);
// 启动线程
thread.start();
// 获取返回值
System.out.println("-----子线程:" + t1.get());
int a = 10;
for (int i = 0; i < a; i++) {
System.out.println(">>>>>a的值"+i+"主线程名称:"+Thread.currentThread().getName());
}
}
@Override
public Integer call() throws Exception {
// 随机数
return new Random().nextInt(11);
}
}
4)、线程池实现多线程
public class ThreadPoolTaskExecutorDemo {
Logger logger = LoggerFactory.getLogger(ThreadPoolTaskExecutorDemo.class);
/**
* 线程池
*/
@Resource
ThreadPoolTaskExecutor threadPoolTaskExecutor;
public static void main(String[] args) {
// 此方法是没有返回值
CompletableFuture.runAsync(() -> { "待处理的逻辑" });
}
}
线程池的七大参数:
corePoolSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务来时,才创建线程去执行任务。默认情况下,线程池的线程数量为0,只有任务来时,才会创建线程,当线程池中的线程数量达到corePoolSize,就会把达到的放到缓存队列当中。
maximumPoolSize:最大核心线程数。表明线程池中能创建的最大数量,此值必须大于等于1
keepAliveTime:空闲的线程保留时间。意思是线程被用过之后,一定时间没有再用后,就会自动放回线程池。
TimeUnit unit:空闲线程保留时间的单位
BlockingQueue:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、
LinkedBlockingQueue、SynchronousQueue可选。
ThreadFactory :线程工程,创建线程的,一般默认即可
RejectedExecutionHandler :拒绝策略。队列已满,而且任务量大于最大线程的异常处理策略。
线程池的配置信息如下:
@Configuration
public class AsyncConfiguration {
/**
* 核心线程数
*/
private int bookCorePoolSize = 3;
/**
* 最大线程数
*/
private int bookMaxPoolSize = 10;
/**
* 队列容量
*/
private int bookQueueCapacity = 100;
/**
* 线程活跃时间(秒)
*/
private int bookKeepAliveSeconds = 100;
/**
* 默认线程名称
*/
private String bookThreadNamePrefix = "priceRelation-sync-%d";
/**
* 接口的线程池
*
* @return TaskExecutor taskExecutor接口
* @since JDK 1.8
*/
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
//newFixedThreadPool
//多线程线程池,内部线程是异步执行的
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数:一般线程池会至少保持这么多的线程数量
executor.setCorePoolSize(bookCorePoolSize);
// 设置最大线程数:线程池最多有这么多的线程数量
executor.setMaxPoolSize(bookMaxPoolSize);
// 设置队列容量
executor.setQueueCapacity(bookQueueCapacity);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(bookKeepAliveSeconds);
// 设置默认线程名称
executor.setThreadNamePrefix(bookThreadNamePrefix);
// 设置拒绝策略
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
//AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
//CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
//DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
//DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
四、锁的概念(Synchronized)
1)、Synchronized是一个安全的锁,锁的是谁,如下:
锁基本概念:总而言之使用Synchronized锁,同一时刻只有一个线程在里面。不会让其他的线程进行访问。
public static void main(String[] args) {
// 这个时候,锁就是,o
Object o = new Object();
synchronized(o){
System.out.println("数据输出");
}
}
public synchronized void m(){
// 就相当于、锁就是 this
synchronized (this){
System.out.println("数据输出");
}
}
public static synchronized void n(){
// 相当于、锁就是类名
synchronized (ThreadPoolTaskExecutorDemo.class){
System.out.println("数据输出");
}
}
2)、不持有锁的线程该咋办?
2.1)、自选等待(忙等待)、轻量级锁
2.2)、等待队列(需要等着OS调度)、重量级锁
轻量级的锁,并不比重量级的锁快?
比如:两三个线程使用轻量级的锁,这个时候速度相对比较快,省略了进入队列,和OS的调度然后再抢锁,循环是消耗CPU的。
比如:有一万个线程在循环的抢锁,是比较占用CPU资源的,这个时候我们就要用到重量级的锁,让锁的线程在队列里等待,这个等待是不消耗CPU资源的。
3)、Synchronized底层原理(重要部分)
3.1)、 什么是CAS?
多线程循环相加处理、按理来说,我们要获取100万,但是执行下面代码,我们确只能拿到几万的数据,这是因为,比如:一个线程获取的数据为9,另一个线程获取的数据也是9,然后线程一回写为10,线程二回写把线程一的值10,又更新了一次,还是10,所以我们拿到的数据不足100万。
注:这添加volatile是不可以的,volatile最主要的功能是两个,
一个是:线程可见性、一个是:指令重排序
private static /*volatile*/ int n = 0;
/**
* 不加锁执行运行,效果是达不到100万的,会造成数据不一致
* @param args
*/
public static void main(String[] args) {
// 启动100个线程
Thread[] threads = new Thread[100];
Object o = new Object();
for (int i= 0; i< threads.length;i++){
threads[i] = new Thread(() -> {
for (int j = 0; j< 10000;j++){
n++;
}
});
}
Arrays.stream(threads).forEach((t) -> t.start());
System.out.println(n);
}
3.1.1)、如何解决这个问题呢?
方法一:添加synchronized 锁
比如:线程一拿到8,线程二也拿到8,我们上了锁以后,线程才能执行++操作,只有线程一把锁释放了,其它线程才能执行。
private static /*volatile*/ int n = 0;
/**
* synchronized 加锁,这里的锁就是 Object 的 o 、是可以实现的但是效率比较低
* @param args
*/
public static void main(String[] args) throws InterruptedException {
// 启动100个线程
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
Object o = new Object();
for (int i= 0; i< threads.length;i++){
threads[i] = new Thread(() -> {
synchronized (o){
for (int j = 0; j< 10000;j++){
n++;
}
latch.countDown();
}
});
}
Arrays.stream(threads).forEach((t) -> t.start());
latch.await();
System.out.println(n);
}
方法二:使用 AtomicInteger 称之为:自旋锁,也叫 CSA。
// 轻量级、无锁、自旋锁
public static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 启动100个线程
Thread[] threads = new Thread[100];
// 新型的锁,等待所有线程执行完后,在执行主线程
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i= 0; i< threads.length;i++){
threads[i] = new Thread(() -> {
for (int j = 0; j< 10000;j++){
// 安全递增,保证数据一致性(自选锁/无锁)
m.incrementAndGet();
}
//标记已经完成一个任务
latch.countDown();
});
}
Arrays.stream(threads).forEach((t) -> t.start());
// 释放锁
latch.await();
System.out.println(m);
}
3.1.2)、CAS 的实现如图:
我们先读一个值,比如为0,我需要把你改成1,在回写的过程中,我需要判断你是否还是0,如果是直接更新为1。如果不是,值为2了,在把值2获取出来,进行操作改成3,回写然后再比较你是否还是2,如果是直接更新,如果不是直接重复之前的操作。称之为自旋锁。
ABA问题:
读值为0,线程一进行++操作改为1,然后回写,这个时候线程二把这个值0,改成了1,然后又改回成1。我们看到的值,就不是原来的0了,只是值是0而已。
解决方法:
第一个:给当前值加一个版本号,1.0,如果有人改过版本号++
第二个:放一个布尔类型。
lock cmpxchg 指令(汇编语言)
cmpxchg 指令 cas 如何保证数据的一致性,非原子性,lock 指令执行 原子性
3.1.3)、锁升级过程
mark word 里面记录了,除了锁的信息,还有GC的信息。
无锁(new)--》 偏向锁 ---》轻量级锁、自旋锁 ---》重量级锁
偏向锁:不用抢锁,偏向于第一个进入的线程。只有一个线程时,效率是最高的时候。70%~80%是只有一个线程在跑的。
轻量级锁:只有有一个线程来抢锁,就会从偏向锁升级为轻量级锁
重量级锁:等待时间过长,线程过多,这个时候就会进入队列等待,升级为重量锁。
比如:CPU最多管理5个线程,然后我们有20个,这个时候就会进入队列等待,不消耗CPU的资源。
3.1.4)、锁消除 lock eliminate
3.1.5)、锁粗化lock coarsening
4)、Volatile的概念