【Java】左右互搏?上下左右互搏?(多线程使用基础)
多线程基础知识
线程和进程
- 进程:在内存中执行的应用程序
- 线程:进程中最小的执行单元
- 作用:负责当前进程中程序的运行,一个进程中至少有一个线程,一个进程还可以有多个线程,这样的应用程序就称之为多线程程序。
- 使用场景:软件中的耗时操作:拷贝大文件,加载大量资源。聊天软件、后台服务器等。
- 一个线程可以干一件事,我们可以同时做多件事,提高CPU的利用率。
并发和并行
- 并行
- 在同一个时刻,有多个执行在多个CPU上面执行(多个人做不同的事情)
- 并发
- 同一个时刻,有多个指令,在单个CPU上(交替)执行
- 细节:
- 之前的CPU是单核,但是在执行多个程序的时候好像是在同时执行,原因是CPU在多个线程之间作高速切换。
- 现在咱们的CPU都为多核多线程的,例如2核4线程,则CPU可以同时运行4个线程,此时不用切换,但是如果多了超出了,CPU就要切换了,所以现在CPU在执行程序的时候并发和并行都存在。
CPU调度
- 分时调度:让所有线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片
- 抢占式调度:多个县城轮流抢占CPU使用权,哪个线程抢到了就先执行,一般都是优先级较高的先抢到CPU使用权几率大。
主线程介绍
- CPU和main方法之间专门为其开辟的线程通道
创建线程的方式(重点)
第一种方式extends Thread
- 定义一个类,继承Thread
- 重写run方法,在run方法中设置线程任务(所谓线程任务指的是此线程要干的具体的事,具体执行的代码)
- 创建自定义线程类的对象
- 调用Thread中的start方法,开启线程,JVM自动调用run方法
Thread中的方法
- 常用方法
void run() 设置线程任务,这个run方法是Thread重写的接口Runnable中的run方法
String getName() 获取线程名字
void setName() 给线程设置名字
static Thread currentThread() 获取正在执行的线程对象(此方法在哪个线程中使用,获取哪个的线程对象)
static void sleep(long millis) 线程睡眠,超时后自动醒来继续执行,传递毫秒值
常用方法使用示例:
public class test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("柳真阳学长的线程");
t1.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "main线程执行了"+i);
}
}
}
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.println(getName()+ "Thread...执行了"+i);
}
}
}
- 优先级&守护&礼让&插入线程方法
void setPriority(int newPriority)
//获取线程优先级
int getPriority()
//设置为守护线程,当非守护线程执行完毕,守护线程停止执行
//注:但守护线程并非立刻结束,当非守护线程结束之后,系统会告知守护线程结束
void setDaemon(boolean on)
//礼让线程(让当前线程让出CPU使用权)
static void yield()
//插入线程,或者插队线程
void join()
获取两个线程优先级
MIN_PRIORITY = 1 最小优先级 1
NORM_PRIORITY = 5 默认优先级 5
MAX_PRIORITY = 10 最大优先级 10
- 优先级相关方法示例:
package com.ham.testhahathread;
public class Test01 {
public static void main(String[] args) {
MyThreadha t1 = new MyThreadha();
t1.setName("lzy");
MyThreadha t2 = new MyThreadha();
t2.setName("dyp");
t1.setPriority(1);
t2.setPriority(10);
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
t1.start();
t2.start();
}
}
package com.ham.testhahathread;
public class MyThreadha extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行一下");
}
}
}
- 守护线程示例:
package com.ham.testhahathread;
public class Test01 {
public static void main(String[] args) {
MyThreadha t1 = new MyThreadha();
t1.setName("lzy");
MyThreadha11 t2 = new MyThreadha11();
t2.setName("dyp");
//将t2设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
package com.ham.testhahathread;
public class MyThreadha11 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "执行一下heiheihie");
}
}
}
package com.ham.testhahathread;
public class MyThreadha extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行一下");
}
}
}
- 礼让线程
场景说明:如果两个线程一起执行,可能会执行一会线程A,再执行一会线程B,或者可能线程A都执行完毕,线程B再执行,那么我们是否可以让两个线程尽可能的平衡一些(尽量让两个线程交替执行)
注意:只是尽可能地平衡,不是绝对的你来我往,有可能线程A先执行。
package com.ham.threadhahha;
public class Test01 {
public static void main(String[] args) {
MyThreadha t1 = new MyThreadha();
t1.setName("lzy");
MyThreadha t2 = new MyThreadha();
t2.setName("dyp");
t1.start();
t2.start();
}
}
package com.ham.threadhahha;
public class MyThreadha extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行一下");
Thread.yield();
}
}
}
创建线程的方式2(实现Runnable接口)
- 创建类,实现Runnable接口
- 重写run方法,设置线程任务
- 利用Thread类中的构造方法:Thread(Runnable target),创建Thread对象(线程对象),将自定义的类当参数传递到Thread的构造中 让我们自己定义的类成为一个真正的线程类对象
- 调用Thread中的start方法,开启线程,JVM自动调用run方法
package com.ham.takeatest;
import com.ham.testhahathread.MyThreadha;
public class test01 {
public static void main(String[] args) {
MyRunnable m = new MyRunnable();
Thread t1 = new Thread(m);
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "Running");
}
}
}
package com.ham.takeatest;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "Running!!!");
}
}
}
两种实现多线程的方式区别
- 继承Thread:继承只支持单继承,有继承的局限性
- 实现Runnable:没有继承的局限性,MyThread extends Fu implements Runnable
匿名内部类创建多线程
- 严格意义上说,匿名内部类方式,不属于创建多线程方式其中之一,因为匿名内部类形式建立在实现Runnable接口的基础上完成的
匿名内部类回顾:
new 接口/抽象类(){
重写方法
}.重写的方法();
==================
接口名/类名 对象名 = new 接口/抽象类(){
重写方法
}
对象名.重写的方法
线程安全
同步代码块
- 线程安全问题示例:
package com.thread.atest;
public class test {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket,"张三");
Thread t2 = new Thread(myTicket,"李四");
Thread t3 = new Thread(myTicket,"lzy");
t1.start();
t2.start();
t3.start();
}
}
public class MyTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
while (ticket>0){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
ticket--;
}
}
}
}
- 原因:CPU在多个线程之间做高速切换导致的
- 使用同步代码块解决线程安全问题
//格式
synchronized(任意对象){
线程可能出现不安全的代码
}
//任意对象:就是我们的锁对象
-
执行:一个线程拿锁之后,会进入到同步代码块中进行,在此期间,其他线程拿不到锁,就进不去同步代码块,需要在同步代码块外面等待排队,需要等着执行的线程执行完毕,除了同步代码块,相当于将锁释放,等待的线程才能抢到锁,才能进入到同步代码块中进行。
-
使用实例:
package com.thread.btest; public class test { public static void main(String[] args) { MyTicket myTicket = new MyTicket(); Thread t1 = new Thread(myTicket,"张三"); Thread t2 = new Thread(myTicket,"李四"); Thread t3 = new Thread(myTicket,"lzy"); t1.start(); t2.start(); t3.start(); } }
package com.thread.btest; public class MyTicket implements Runnable{ int ticket = 100; //任意new一个对象 Object obj = new Object(); @Override public void run() { while (ticket>0){ try { Thread.sleep(100L); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (obj) { if (ticket > 0) { ticket--; System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票"); } } } } }
同步方法
普通同步方法_非静态
格式:
修饰符 synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
默认锁:this
使用示例:
public class test {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket,"张三");
Thread t2 = new Thread(myTicket,"李四");
Thread t3 = new Thread(myTicket,"lzy");
t1.start();
t2.start();
t3.start();
}
}
public class MyTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
while (ticket>0){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method01();
}
}
public synchronized void method01(){
if(ticket > 0){
ticket--;
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
}
}
}
静态同步方法
格式:
修饰符 static synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
默认锁:class对象
线程生命周期
线程状态介绍
- 当线程被创建并且启动之后,它并非一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态?在API的java.lang.Thread.State这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW | 线程刚被创建,但并未启动,并未调用start方法 |
Runnable | 线程可以在JVM中运行的状态,可能正在运行自己的代码 |
Blocked | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当线程持有锁时,该线程将变成Runnable状态 |
Waiting | 一个线程在等待另一个线程执行唤醒动作,该线程进入Waiting状态,进入这个状态之后不可以自动唤醒,必须等待另一个线程调用notify或者notifyALL方法才能够唤醒。 |
TimedWaiting | 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait。 |
Terminated | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或者调用过时方法stop()。 |
线程状态图
等待唤醒机制
- 要求:一个线程生产,另一个线程消费,不能连续消费 -> 等待唤醒机制(生产者,消费者)(线程之间的通信)
方法 | 说明 |
---|---|
void wait() | 线程等待,等待过程中会释放锁,需要被其他线程调用notify方法将其唤醒,唤醒之后重新抢锁执行。 |
void notify() | 线程唤醒,一次唤醒一个等待线程;如果有多条线程等待,则随机唤醒一条等待线程。 |
void notifyAll() | 唤醒所有等待线程。 |
wait和notify方法需要锁对象调用,所以需要用到同步代码块中必须为同一个锁对象
等待唤醒案例如下:包子铺生产者消费者案例
package com.thread.waitornotifytest;
public class test {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
Thread t1 = new Thread(product);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
package com.thread.waitornotifytest;
public class Consumer implements Runnable{
private BaoZiPu baoZiPu;
public Consumer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
if(baoZiPu.isFlag() == false){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
baoZiPu.getCount();
baoZiPu.setFlag(false);
baoZiPu.notify();
}
}
}
}
package com.thread.waitornotifytest;
import java.awt.*;
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
if(baoZiPu.isFlag() == true){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
baoZiPu.setCount();
baoZiPu.setFlag(false);
baoZiPu.notify();
}
}
}
}
package com.thread.waitornotifytest;
public class BaoZiPu {
//代表包子的count
private int count;
private boolean flag;
public BaoZiPu(boolean flag) {
this.flag = flag;
}
public BaoZiPu(int count) {
this.count = count;
}
public BaoZiPu() {
}
public void setCount() {
count++;
System.out.println("生产了...第" + count + "个包子");
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void getCount() {
System.out.println("消费了....第" + count + "个包子");
}
public boolean isFlag() {
return flag;
}
}
Lock锁
- Lock是一个接口
- 实现类:ReentrantLock
- 方法:
- lock() 获取锁
- unlock() 释放锁
synchronized:不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放锁对象。
Lock:通过两个方法控制需要被同步的代码,使用上更加灵活。
package com.thread.btest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTicket implements Runnable{
int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (ticket>0){
try {
Thread.sleep(10L);
lock.lock();;
if(ticket > 0){
ticket--;
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
}
package com.thread.btest;
public class test {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket,"张三");
Thread t2 = new Thread(myTicket,"李四");
Thread t3 = new Thread(myTicket,"lzy");
t1.start();
t2.start();
t3.start();
}
}
Callable接口
Callable是一个接口,类似于Runnable
方法:
V call() -> 设置线程任务,类似于run方法
call方法和run方法的区别:
- 相同点:都用于设置线程任务
- 不同点:call方法有返回值,而且有异常可以throws,run没有返回值,也不可以throws
- 1. 叫做泛型 2. 泛型用于指定我们操作什么类型的数据,<>中只能写引用数据类型,如果反省不写,默认是Object类型数据。 3. 实现Callable接口时,执行泛型是什么类型的,重写的call方法,返回值就是什么类型。
- 获取call方法返回值:FutureTask
- FutureTask 实现了一个接口:Future
- FutureTask 中有一个方法:V get() -> 获取call方法的返回值
- FutureTask 实现了一个接口:Future
使用示例:
package com.thread.CallableTest;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
//创建Thread类对象 -> Thread(Runnable target)
Thread t1 = new Thread(stringFutureTask);
t1.start();
System.out.println(stringFutureTask.get());
}
}
package com.thread.CallableTest;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "柳真阳学长别压力我了";
}
}
线程池
问题:之前来一个线程任务,就需要创建一个线程对象去执行,用完还要销毁线程对象,如果线程任务多了,就需要频繁创建线程对象和销毁线程对象
线程池操作流程示例:
- 创建线程池对象,指定池子中最多能有多少条线程对象 -> 2个
- 来了第一个线程任务,看池子中有无线程对象,如果没有,则创建线程对象,给线程任务用,用完还回去
- 来了第二个线程任务,如果有直接用,用完还回去,如果没有则创建线程对象,给线程任务用,用完还回去。
- 来了第三个线程任务,如果池子中有空闲线程,直接用,用完还回去,如果没有,则等待之前线程任务执行完毕,归还线程对象再使用,用完还回去。
线程池的使用
-
如何创建线程池对象:用工具类 -> Executors
-
获取线程池对象:Executors中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
- 参数:指定线程池中最多创建的线程对象条数
- 返回值ExecutorService 是线程池,用于管理线程对象
执行线程任务:ExecutorService中的方法
Future<?> submit(Runnable task) 提交一个Runnable任务
Future<T> submit(Callable<T> task) 提交一个Callable任务用于执行
-
submit方法的返回值:Future接口。
- 用于接收run方法或者call方法返回值的,但是run方法没有返回值,所以可以不用Future接收,执行call方法需要用Future接收。
- Future中有一个方法:V get() 用于获取call方法返回值或者run方法返回值
-
ExecutorService中的方法:
void shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
定时器Timer
- 概述:定时器
- 构造:Timer()
方法:
void schedule(TimerTask task,Date firstTime,long period)
task: 抽象类,是Runnable的实现类
firstTime:从什么时间开始执行
period:每隔多长时间执行一次,设置的是毫秒值
package com.thread.Timer;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Demo01Timer {
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("haha");
}
},new Date(),1000L);
}
}