Java基础学习多线程
- 一、多线程
- 1.1 什么是多线程
- 1.2 多线程的两个概念
- 1.2.1 并发
- 1.3 多线程的实现方式
- 1.4 多线程的成员方法
- 1.5 线程的生命周期
- 二、线程安全
- 1.6 同步方法
- 1.7 锁lock
- 1.8 死锁
- 1.8 生产者和消费者 (等待唤醒机制)
- 1.9 等待唤醒机制(阻塞队列方式实现)
- 1.10 线程状态
- 二、线程池
- 2.1 线程池的概述
- 2.2 自定义线程池
- 2.3 线程池到底多大才合适
一、多线程
1.1 什么是多线程
线程:
线程
是操作系统能够进行运算调度的最小单位
。它被包含在进程
之中,是进程中的实际运作单位.
应用软件中互相独立,可以同时运行的功能
进程:
进程是程序的基本执行实体
多线程的应用场景:
- 软件中的耗时操作
- 拷贝、迁移大文件
- 加载大量的资源文件
- 所有的聊天软件
- 所有的后台服务器
……
同时这些多项任务,提升运行效率
1.2 多线程的两个概念
1.2.1 并发
并发:在同一时刻,有多个指令在单个CPU上交替
执行
并行:在同一时刻,有多个指令在多个CPU上同时
执行
1.3 多线程的实现方式
- 继承Thread类的方式进行实现
多线程的第一种启动方式:
- 自己定义一个类继承Thread
- 重写run方法
- 创建子类的对象,并启动线程
重写的Run方法:
package MyThreads;
//重写run方法(Thread)
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "HelloWorld!!");
}
}
}
启动的线程:
package MyThreads;
//新建Thread
public class Dom1 {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//给线程添加名字
t1.setName("线程1");
t2.setName("线程2");
//启动两个线程
t1.start();
t2.start();
}
}
- 实现Runnable接口的方式进行实现
多线程的第二种启动方式:
- 自己定义一个类实现Runnable接口
- 重写里面的run方法
- 创建自己的类的对象
- 创建一个Thread类的对象,并开启线程
执行代码:
package MyThreads;
//创建线程二
//利用runnable接口进行创建
/*
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread类的对象,并开启线程
*
* */
public class Dom2 {
public static void main(String[] args) {
//创建线程需要执行的任务
MyThread2 mr = new MyThread2();
//创建线程执行mr任务
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程添加名字
t1.setName("线程一");
t2.setName("线程二");
//开启线程
t1.start();
t2.start();
}
}
接口代码:
package MyThreads;
//创建线程二
//利用runnable接口进行创建
/*
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread类的对象,并开启线程
*
* */
public class Dom2 {
public static void main(String[] args) {
//创建线程需要执行的任务
MyThread2 mr = new MyThread2();
//创建线程执行mr任务
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程添加名字
t1.setName("线程一");
t2.setName("线程二");
//开启线程
t1.start();
t2.start();
}
}
- 利用Callable接口和Future接口方式实现
多线程的第三种实现方式:
特点:可以获取到多线程运行的结果
- 创建一个类MyCallable实现Callable接口
- 重写call (是有返回值的,表示多线程运行的结果 )
- 创建MyCallable的对象 (表示多线程要执行的任务)
- 创建FutureTask的对象 (作用管理多线程运行的结果)
- 创建Thread类的对象,并启动 (表示线程)
对象:
package MyThreads;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
//线程创建3
/*
* 多线程的第三种实现方式:
特点:可以获取到多线程运行的结果
1。创建一个类MyCallable实现Callable接口
* 2。重写ca11 (是有返回值的,表示多线程运行的结果 )
3。 创建MyCallable的对象 (表示多线程要执行的任务)
* 4。创建FutureTask的对象 (作用管理多线程运行的结果)
* 5。创建Thread类的对象,并启动 (表示线程)
* */
public class Dom3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyCallable的对象 (表示多线程要执行的任务)
MyCallable mr = new MyCallable();
//创建FutureTask的对象 (作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mr);
//创建Thread类的对象,并启动 (表示线程)
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取返回值
Integer result = ft.get();
System.out.println(result);
}
}
接口:
package MyThreads;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
总结:
按照不同的需求去完成我们的线程创建
1.4 多线程的成员方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(string name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
细节:(前面四个成员方法)
细节1:
如果我们没有设置名字,JVM会自动给我们设置Thread-x的名字,x从0开始。
如果我们要设置名字。可以通过set设置,也可以用继承的Thread的对象,重写Thread的构造方法,利用有参构造细节2;
当启动JVM时,就会创建多个线程
,我们以前所写的代码都在main线程
里面进行的
细节3:
sleep:哪条线程执行到该代码就会在这条线程上停留
测试代码:
package MyThreadThod;
//成员方法演示
/*
* 1. 设置名字,返回名字
* 2. 获取当前的线程名字
* 3. 让线程睡眠(单位毫秒)
* */
public class Dom1 {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread("lisi");
MyThread t2 = new MyThread("zhangsan");
//开启线程
t1.start();
t2.start();
}
}
继承Thread代码:
package MyThreadThod;
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//睡眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+"@"+i);
}
}
}
成员方法优先级
范围: 1~10
优先级越高,就代表的执行他的概率就越高
,但是不是代表优先级越高,就先执行该代码,执行的顺序是随机的
,但是优先级越大,随机的概率就越大
默认的优先级: 5
package MyThreadThod;
//成员方法,优先级
public class Dom2 {
public static void main(String[] args) {
//创建多线程执行的任务
MyRunnable mr = new MyRunnable();
//创建Thread对象,执行任务
Thread t1 = new Thread(mr);
//设置优先级
t1.setPriority(10);
//启动
t1.start();
}
}
守护线程:
这是表示
“备胎守护线”
,当所守护的对象不存在的时候就没有存在的必要了,就会提前结束线程
package MyThreadThod;
//成员方法:守护线程
public class Dom3 {
public static void main(String[] args) {
//创建Thread对象
MyThread t1 = new MyThread();
MyThread2 t2 = new MyThread2();
//将第二个设置为守护线程
t2.setDaemon(true);
t1.setName("女神");
t2.setName("二货");
t2.start();
t1.start();
}
}
出让线程:
让执行的线程相对平均
Thread.yieId();
插入线程:
让该线程执行完后再执行该代码
package MyThreadThod;
import MyThreads.MyThread1;
//插入代码
public class Dom4 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyThread1();
t1.setName("洋芋");
t1.start();
t1.join();
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}
1.5 线程的生命周期
二、线程安全
买票引发的安全问题:
- 相同的票出现了多次
- 出现了超出范围的票
解决方法:
- 利用锁
特点1: 锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
代码实现
package MySafeThread.safe1;
public class MySafe extends Thread{
static int ticket = 0;
//锁对象必须是唯一的
static Object obj = new Object();
@Override
public void run() {
//创建锁
//作用:如果有一个进入到了线程当中就会,就不会让其他的线程进入,直到
//当该线程完成过后,重写再抢夺CPU
while (true){
//创建锁
synchronized (obj){
if (ticket<100){
//每个10毫秒在进行买票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "正在卖"+ticket+"张票了!!!!!!!!!");
}else {
break;
}
}
}
}
}
锁对象必须得是唯一的
锁不唯一就表示打开这扇门的钥匙有多个,不能让该代码执行完后,才能让其他的线程抢夺,这样会导致多执行,重复的效果
一般情况下用:当前代码的字节码文件.class
来表示唯一对象
1.6 同步方法
就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {...}
特点1: 同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
里面在不同状态下调用的对象也不一样:
非静态:
this
静态:
当前类的字节码文件对象
Stirngbuilder 与 Stringbuffer 的区别:
Stringbuffer
在多线程下相对于Stirngbuilder
较为安全- 所以在单线程的情况下一般采用
Stringbuilder
主要修改的代码
package MySafeThread.safe2;
import java.util.Timer;
public class MyRunnable2 implements Runnable{
int ticket = 0;
@Override
public void run() {
//1. 循环
//2. 同步方法
//3.判断是否完成,未完成,执行下列代码
//4.判断是否完成,已完成,执行下列代码
while (true){
if (method()) break;
}
}
//创建同步方法:这里是非静态的调用时为:this
private synchronized boolean method() {
if (ticket == 100) {
return true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票了!!!!!");
}
return false;
}
}
1.7 锁lock
定义:
虽然我们可以理解同步代码块和同步方法的锁对象问题但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock中提供了获得锁和释放锁的方法
- void lock(): 获得锁
- void unlock(): 释放锁
手动上锁、手动释放锁
Lock是接口不能直接实例化
这里采用它的实现染ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():
创建一个ReentrantLock的实例
1.8 死锁
两个锁互相嵌套导致,两个锁都在等待着对方放下锁,但是他们释放锁的情况是执行该所下面的代码才能释放,这样就导致程序卡死出不去
。
死锁实列:
在这里插入代码片
所以我们要在写代码的时候,千万不要进行锁的嵌套写法,一不小心就会掉进坑里面
package MySafeThread.BugLock;
public class BUGLock extends Thread {
static Object obj1 = new Object();
static Object obj2 = new Object();
@Override
public void run() {
if ("A".equals(getName())){
synchronized (obj1){
System.out.println("拿到了线程A,准备拿线程B");
synchronized (obj2){
System.out.println("拿到了线程B,结束");
}
}
} else if ("B".equals(getName())) {
if ("B".equals(getName())){
synchronized (obj2){
System.out.println("拿到了线程B,现在拿线程A");
synchronized (obj1){
System.out.println("拿到了线程A,现在拿线程B");
}
}
}
}
}
}
1.8 生产者和消费者 (等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作的模式
形象化理解:
方法名称 | 说明 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify() | 随机唤醒单个线程 |
void notifyAll() | 唤醒所有线程 |
启动窗口:
package MyLock.WaitAndNotify;
import java.io.FileOutputStream;
public class waitNotiyDom {
public static void main(String[] args) {
//创建厨师和吃货的对象
Cook cook = new Cook();
FoodAddict addict = new FoodAddict();
//设置名字
cook.setName("厨师");
addict.setName("吃货");
cook.start();
addict.start();
}
}
厨师窗口:
package MyLock.WaitAndNotify;
public class Cook extends Thread{
@Override
public void run() {
//循环
while (true){
//创建同步代码块
synchronized (Desk.lock){
//判断,没有执行到尾部
if (Desk.MaxFood == 0){
break;
}else {
//执行到末尾
//首先判断状态值是否为0,是否有面条
if (Desk.Food == 1){
//是1,就执行等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//是0,就做食物,释放所有等待代码
System.out.println("厨师正在做食物");
System.out.println("厨师已做完,请吃");
//将状态码变为1
Desk.Food =1;
//释放所以等待代码
Desk.lock.notifyAll();
}
}
}
}
}
}
吃货窗口:
package MyLock.WaitAndNotify;
public class FoodAddict extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
//判断是否还能吃下去
if (Desk.MaxFood == 0){
//不能吃下去了
System.out.println("吃货吃不下了,要g了");
break;
}else{
//还能继续吃
//判断桌子上面是否有吃的
if (Desk.Food == 0){
//没有吃的就执行等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//有吃的
//将吃的碗数减1
Desk.MaxFood--;
System.out.println("吃货正在吃面条,还可以吃"+Desk.MaxFood+"碗");
//已经吃完,释放所有的代码,叫厨师吃饭了
Desk.lock.notifyAll();
//食物清空
Desk.Food =0;
}
}
}
}
}
}
桌子窗口:
package MyLock.WaitAndNotify;
//表示桌子,来放置面条的状态,达到谁去执行的效果
public class Desk{
//创建面的状态 , 0:没有面条 1:有面条
public static int Food = 0;
//创建吃货吃的最多碗
public static int MaxFood = 10;
//创建锁
public static Object lock = new Object();
}
1.9 等待唤醒机制(阻塞队列方式实现)
实现类:
ArrayBlockingQueue :底层是数组,有界限
LinkedBlockingQueue:底层是链表,无界但不是真正的无界最大为int的最大值。
厨师类
package MyLock.ArrayBlockingQueues;
import java.util.concurrent.ArrayBlockingQueue;
public class queueCooker extends Thread{
ArrayBlockingQueue<String> queue;
public queueCooker(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
//不需要再写锁了,因为put当中写了
try {
queue.put("面条");
System.out.println("厨师做了面");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
吃货类
package MyLock.ArrayBlockingQueues;
import java.util.concurrent.ArrayBlockingQueue;
public class queueAddict extends Thread{
ArrayBlockingQueue<String> queue;
public queueAddict(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
String food = queue.take();
System.out.println("吃货在吃"+food);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
执行类:
package MyLock.ArrayBlockingQueues;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
//阻塞队列方式实现等待唤醒任务
public class ArrayBlockingQueueDom {
public static void main(String[] args) {
//创建阻塞队列方式(唯一)
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建两个变量,赋名字,并且把阻塞队列传入
queueAddict addict = new queueAddict(queue);
queueCooker cooker = new queueCooker(queue);
addict.setName("吃货");
cooker.setName("厨师");
//开启线程
addict.start();
cooker.start();
}
}
1.10 线程状态
六种红色的状态
新建状态(NEW ) --> 创建线程对象
就绪状态(RUNNABLE ) --> start方法
阻塞状态( BLOCKED ) --> 无法获得锁对象
等待状态 (WAITING ) --> wait方法
计时等待 (TIMED WAITING ) --> sleep方法
结束状态(TERMINATED ) --> 全部代码运行完毕
二、线程池
2.1 线程池的概述
- 以前写多线程的弊端
- 用到线程的时候就创建
- 用完之后线程消失
会造成资源的浪费
所以现在出现了线程池来存储线程
线程池的核心原理:
- 创建一个池子,池子中是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
创建线程池的方法:
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
package MythreadPool.dom1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//创建线程池,初识线程池
public class Dom1 {
public static void main(String[] args) {
//创建无上限的线程池
ExecutorService pool1 = Executors.newCachedThreadPool();
//创建有上限的线程池(3个)
// ExecutorService pool2 = Executors.newFixedThreadPool(3);
//提交任务
pool1.submit(new MyRunnable1());
pool1.submit(new MyRunnable1());
pool1.submit(new MyRunnable1());
pool1.submit(new MyRunnable1());
}
}
package MythreadPool.dom1;
public class MyRunnable1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "-----------" + i);
}
}
}
2.2 自定义线程池
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略 : 丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardoldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
理解:
自定义线程池:
核心元素一: 正式员工数量 --------------> 核心线程数量(不能小于0)
核心元素二: 餐厅最大员工数 ---------------> 线程池中最大线程的数量(最大数量 >= 核心线程数量)
核心元素三: 临时员工空闲多长时间被辞退(值) --------> 空闲时间 (值) (不能小于0)
核心元素四: 临时员工空闲多长时间被辞退(单位) -------> 空闲时间(单位) (用TimeUnit指定)
核心元素五: 排队的客户 --------> 阻塞队列 (不能为null)
核心元素六: 从哪里招人 ---------> 创建线程的方式(不能为null)
核心元素七: 当排队人数过多,超出顾客请下次再来(拒绝服务) ------> 要执行的任务过多时的解决方案(不能为null)
package MythreadPool.dom2;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//自定义线程池
public class dom2 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3 , //核心线程数量(表示的正式员工)
6, //最大线程数量(总员工数量)
60,//空闲线程最大的存活时间
TimeUnit.SECONDS, //表示当前最大存活的时间单位
new ArrayBlockingQueue<>(3),//排队的客户
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//拒绝方法
);
}
}
总结:
- 创建一个空的池子
- 有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程
不断的提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队
- 当核心线程满,队伍满时,会创建临时线程
- 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略
2.3 线程池到底多大才合适
CPU 密集型运算
--------> 最大并行数 +I
l/0 密集型运算
----------> 最大并行数 * ”期望 CPU利用率 * 总时间(CPU计算时间+等待时间) / CPU计算时间
例如: 4核8线程