多线程 JUC
1.什么是多线程?
什么是进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
可以简单理解成,打开一个软件,就是在执行一个进程
什么是线程
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。
如图所示,打开某安全软件,里面有如上几个功能。
可以简单理解成:应用软件中相互独立,可以同时运行的功能。
如果进程中线程较多,就形成了多线程。
有了多线程之后,我们就可以让程序同时做多件事情。
那么多线程如何执行呢?
简单理解:
如上图所示,单线程程序每执行一条就会稍微摸摸鱼~这无疑会降低了 CPU 的效率。
那么把等待的时间应用起来,就是多线程的特点。
多线程的应用场景
例如,原神,启动!打开一个大型游戏,桌面会显示一个进度条,加载大量的资源文件,此时如果单单做这件事情,就未免太慢太枯燥了。所以在加载资源文件的过程中,还可以去启动另一个线程去做别的事情,例如检查一下版本号、播放背景音乐等等。
2.并发、并行
-
并发:在同一时刻,有多个指令在单个 CPU 上交替进行。
边打游戏、边拿瓶可乐、边看看微信。
CPU 就是在这三条线程中交替进行~
-
并行:在同一时刻,有多个指令在多个 CPU 上同时进行。
CPU: 2 核 4 线程。代表CPU 能同时跑 4 条线程。如果只需要执行 4 条线程,很显然可以并行执行。但是如果所需的量大于 4 条线程,就需要并发配合并行啦。
并发与并行同时发生
3.多线程的实现方式
当 JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫 main 线程
他的作用就是去调用 main 方法,并执行里面的代码
-
继承 Thread 类的方式进行实现
如果你想拥有一条线程,就创建一个 Thread 的对象就行。
运行结果
-
实现 Runnable 接口的方式进行实现
实现 Runnable 接口的类,该类实现 run 方法。
-
利用 Callable 接口和 Future 接口方式实现
特点:可以获得多线程运行的结果
4.多线程中的常用成员方法
线程的优先级
Java 执行的是抢占式调度的线程程切换方法,完全随机。优先级越高,获取线程的概率越高!是概率,并非优先级越高就会一定抢到。
当线程没有设置优先级时,默认是 5。 优先级的级别为 1-10
- setPriority(int):设置线程的优先级
- final int getPriority():获取线程的优先级
守护线程
应用场景: 当两个人在聊天,可以把聊天当作一个线程、传输当作一个线程。当把聊天页面关闭了,传输文件停掉,就可以用到守护线程。
5.线程的生命周期
6.线程的安全问题
多个线程操作同一个数据,会出现问题
线程执行时,有随机性
同步代码块
两个小细节
-
如果把同步代码快放在 while 循环语句外面,会造成线程一直在某个线程执行,直到 while 循环结束。
-
synchronized 后面的锁对象,一定要是唯一的!不然锁相当于没有~
可以用MyThread.class 作为锁对象,铁定唯一~
同步方法
选中里面的方法,command + L + M,登登登登:
这就抽取成了一个同步方法。
Lock 锁
Lock 锁的使用很简单,Lock lock = new ReentrantLock(); 注意不能直接 new Lock,因为这是一个接口,必须 new 其实现类。
使用 Lock 锁的实例以及注意事项:
如图所示,乍一看这些个代码没什么问题!
但是程序运行起来你会发现:
有问题:程序一直没有停止,虽然已经输出完毕了。 那么是为什么呢?
仔细分析一下代码发现:当 ticket == 10 时,程序直接 break 了跳出了 while 循环。那么很显然,就没有执行到 lock .unlock()这句解锁代码,所以线程一直拿着锁没有放。
那么如何解决呢?有个不够优雅的办法就是在 break 语句之前加上一句lock .unlock(),这样做确实是可以达到目的,但是 unlock 一般用于结尾使得代码结构好看。
有一个办法就是一定能执行到 unlock 方法,就是用 try、 catch 、 finally 中的 finally 语句。
死锁
死锁是一个错误,记住一句话,写代码的时候,不要让两个锁嵌套起来
生产者消费者
让两条线程轮流执行~原理:
理想的情况下,生产者线程先拿到了 CPU 控制权,生产了资源,再供给消费者
-
消费者等待:消费者先拿到控制权,只能先等待 --wait。 生产者再拿到控制权,生产好了执行 唤醒---notify
-
生产者等待: 生产者先拿到控制权,下一次还是生产者拿到控制权,只能先等待 --wait 。
常见方法
代码实现
-
桌子
-
生产者
-
消费者
多线程的 6 种状态
线程池
为什么要有线程池呢?
线程池的核心原理
线程池的代码实现
-
创建线程池
-
提交任务
-
所有的任务执行完毕,关闭线程池
线程池一般是不会销毁的,因为服务器一般不会关闭~
创建线程池对象的方法:
package com.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class mythreadpool {
public static void main(String[] args) {
/*
* 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
* 2.public static ExecutorService newCachedThreadPool(int nThreads)//有上限的线程池
* */
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();
}
}
package com.threadpool;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " ---");
}
}
上面两段代码为线程池的基本操作,该实现相当于在线程池中创造了四个线程,没有达到复用的效果。
public class mythreadpool {
public static void main(String[] args) throws InterruptedException {
/*
* 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
* 2.public static ExecutorService newCachedThreadPool(int nThreads)//有上限的线程池
* */
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
//3.销毁线程池
pool1.shutdown();
}
}
在每段提交任务之后,设置一个休眠,保证前一个任务已经执行完,所以下一次执行就会实现复用。
有上限的线程池
package com.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class mythreadpool {
public static void main(String[] args) throws InterruptedException {
/*
* 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
* 2.public static ExecutorService newFixedThreadPool(int nThreads)//有上限的线程池
* */
//1.获取线程池对象
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();
}
}
执行结果:
可见,最多只创建了三个线程
自定义线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor();
实际上就是调用这个类来实现自己定义线程池,需要输入 7 个不同的参数!
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
示例
比喻:
线程池多大合适呢
以四核八线程的 CPU 来解释:
事实上可以简单理解成 CPU 有四个核心,通过超线程技术,每个核心虚拟一分为二,可以同时处理八条线程。
最大并行数为 8.
//获取执行线程数量
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);
可以用 thread dump 工具测试