Java多线程
前言
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
1、线程 进程 程序
进程是一个应用程序,就是大家所说的软件(QQ、微信、王者荣耀。。。)
线程是程序执行的最小单位,可以理解为执行一个软件中某一个功能
程序是为完成特定任务、用某种语言编写的一组指令的集合
cpu
CPU的中文名称是中央处理器,是进行逻辑运算用的,我们的线程就是运行在cpu上,在我们的多线程上面,单核cpu只能执行一个线程任务,多核cpu才可以执行多个线程任务,才能更好的发挥多线程的效率
一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程
串行和并行
为什么需要使用到多线程
目的就是为了提高程序开发的效率
使用多线程的优点
1、 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
2、 提高计算机系统CPU的利用率
3、 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时使用多线程
1、程序需要同时执行两个或多个任务
2、程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
3、需要一些后台运行的程序时
2、线程的创建和使用
方法名 | 说明 |
---|---|
Thread() | 创建新的Thread对象 |
Thread(String threadname) | 创建线程并指定线程实例名 |
Thread(Runnable target) | 指定创建线程的目标对象,它实现了Runnable接口中的run方法 |
Thread(Runnable target, String name) | 创建新的Thread对象 |
创建线程的两种方式
方式一:继承Thread类
- 定义子类继承Thread类
- 子类中重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象
- 调用线程对象start方法:启动线程,调用run方法
package com.nnfx.threads;
/**
* @author 杰仔正在努力
* @create 2022-12-10 15:18
* 使用Thread创建多线程
*/
public class Thread01 extends Thread{
/**
* 线程的执行的代码 就是在run方法中 执行完毕 线程死亡
*/
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "<我是子线程>");
try {
//当前线程阻塞3秒
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "阻塞完毕!");
}
public static void main(String[] args) {
//启动线程 调用start方法 不是run方法
System.out.println(Thread.currentThread().getName());
//调用start()线程不是立即被cpu调度执行 而是先等待cpu调度 线程从就绪状态转向运行状态
new Thread01().start();
new Thread01().start();
System.out.println("主线程执行完毕");
}
}
注意点:
1、自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
2、 run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定
3、启动多线程,必须调用start方法
4、一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”
方式二:实现Runnable接口
1、定义子类,实现Runnable接口
2、子类中重写Runnable接口中的run方法
3、 通过Thread类含参构造器创建线程对象
4、 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
5、调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法
package com.nnfx.threads;
/**
* @author 杰仔正在努力
* @create 2022-12-10 15:31
* 实现Runnable接口、lambda表达式、匿名内部类创建多线程
*/
public class ThreadRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "<我是子线程>");
}
public static void main(String[] args) {
//启动多线程
// new Thread(new ThreadRunnable()).start();
//使用匿名内部类的形式创建多线程
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName() + "<我是子线程>");
// }
// }).start();
//lambda表达式
new Thread(() -> System.out.println(Thread.currentThread().getName() + "<我是子线程>"),"lambda表达式").start();
}
}
方法名 | 说明 |
---|---|
start() | 启动线程,并执行对象的run()方法 |
run() | 线程在被调度时执行的操作 |
String getName() | 返回线程的名称 |
void setName(String name) | 设置该线程名称 |
static Thread currentThread() | 获取当前线程 |
static void yield() | 线程让步 |
static void sleep(long millis) | 指定时间 |
线程的调度(了解)
抢占式调度:
对于线程优先级高的,使用优先调度的抢占式策略
均分式调度:
每个线程占有cpu时间片时间长度一样
线程的优先级:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority(): 返回线程优先值
setPriority(int newPriority): 改变线程的优先级
3、线程的生命周期
1、新建状态
2、就绪状态
3、运行状态
4、阻塞状态
5、死亡状态
4、线程安全
什么是线程安全问题
多线程同时对同一个全局变量做写的操作,可能会受到其他线程的干扰,就会发生线程安全性问题
如何解决线程安全问题
使用线程排队,用排队的方式解决线程安全问题
这种机制叫做—>线程同步机制
Synchronized的使用方法
线程同步机制语法:
synchronized (对象){
// 需要被同步的代码;
}
同步锁的实现思想
在同一个jvm中,多个线程需要竞争锁的资源,但是最终就只能有一个线程能够获取到锁,多个线程去抢一把锁,谁能过获取到锁,谁就能往下执行代码,如果没有获取锁成功 中间需要经历锁的升级过程如果一致没有获取到锁则会一直阻塞等待
假设有线程t1、t2、t3三个线程
如果线程t1获取锁成功,但是线程t1一直不释放锁,那么线程t2、t3就一直获取不到锁,就会一直处于阻塞等待状态
synchronized三种用法
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码快前要获得 给定对象 的锁
public class ThreadCount implements Runnable {
private static Integer count = 100;
private String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private void cal() {
synchronized (this) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
Thread thread1 = new Thread(threadCount);
Thread thread2 = new Thread(threadCount);
thread1.start();
thread2.start();
}
}
修饰实例方法:作用于当前实例加锁,进入同步代码前要获得 当前实例的锁
public class ThreadCount implements Runnable {
private static Integer count = 100;
private String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private synchronized void cal() {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
Thread thread1 = new Thread(threadCount);
Thread thread2 = new Thread(threadCount);
thread1.start();
thread2.start();
}
}
修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得 当前类对象的锁默认使用当前类的类名.class 锁
public class ThreadCount implements Runnable {
private static Integer count = 100;
private static String lock = "lock";
@Override
public void run() {
while (count > 1) {
cal();
}
}
private static void cal() {
synchronized (ThreadCount.class) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
count--;
System.out.println(Thread.currentThread().getName() + "," + count);
}
}
public static void main(String[] args) {
ThreadCount threadCount1 = new ThreadCount();
ThreadCount threadCount2 = new ThreadCount();
Thread thread1 = new Thread(threadCount1);
Thread thread2 = new Thread(threadCount2);
thread1.start();
thread2.start();
}
}
关于同步方法的总结:
1、同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
2.、 非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
synchronized死锁问题
1、不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2、出现死锁后,不会出现异常,不会出现提示,只是所有的程都处于阻塞状态,无法继续
解决方法:
1、尽量减少同步资源的定义
2、尽量避免嵌套同步
Lock锁
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码; }
finally{
lock.unlock();
}
}
}
使用Condition
实现等待/通知 类似于 wait()和notify()及notifyAll()
public class Thread10 {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread10 thread10 = new Thread10();
try {
thread10.print();
Thread.sleep(3000);
thread10.signal();
} catch (Exception e) {
}
}
public void print() {
new Thread(() -> {
try {
// 释放锁 同时当前线程阻塞
lock.lock();
System.out.println(Thread.currentThread().getName() + ",1");
condition.await();
System.out.println(Thread.currentThread().getName() + ",2");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
public void signal() {
try {
lock.lock();
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Lock锁和synchronized锁对比
1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
2、Lock只有代码块锁,synchronized有代码块锁和方法锁
3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多 的子类)
关于线程的sleep方法
1、静态方法:Thread.sleep(3000)
2、参数是毫秒
3、作用:
让当前线程进入阻塞状态
,放弃当前cpu的使用权,转让给其他线程进行使用
package com.nnfx.threads;
/**
* @author 杰仔正在努力
* @create 2022-12-16 17:08
*/
public class Test5 {
public static void main(String[] args) {
for (int i = 1;i < 10;i++) {
System.out.println(Thread.currentThread().getName()+"---"+ i);
//每三秒钟打印一个数字
try {
Thread.sleep(3000);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
如何安全的停止一个线程
1、Interrupt(线程中止)
Interrupt 打断正在运行或者正在阻塞的线程
public class Thread06 extends Thread {
@Override
public void run() {
while (true) {
// 如果终止了线程,则停止当前线程
if (this.isInterrupted()) {
break;
}
}
}
public static void main(String[] args) {
Thread06 thread06 = new Thread06();
thread06.start();
try {
Thread.sleep(1000);
} catch (Exception e) {
}
thread06.interrupt();
}
}
2、标志位
增加一个判断,用来控制线程执行的中止
public class Thread07 extends Thread {
private volatile boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
}
}
public static void main(String[] args) {
Thread07 thread07 = new Thread07();
thread07.start();
// thread07.isFlag = false;
}
}
多线程yield
主动释放cpu执行权
1、多线程yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程
2、具体的实现依赖于底层操作系统的任务调度器
public Thread02(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (i == 30) {
System.out.println(Thread.currentThread().getName() + ",释放cpu执行权");
this.yield();
}
System.out.println(Thread.currentThread().getName() + "," + i);
}
}
public static void main(String[] args) {
new Thread02("mayikt01").start();
new Thread02("mayikt02").start();
}
守护线程与用户线程
线程分为两种类型
1、用户线程
2、守护线程
通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程
守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程
.用户线程是独立存在的,不会因为其他用户线程退出而退出
public class Thread01 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ",我是子线程");
} catch (Exception e) {
}
}
});
thread.setDaemon(true);
thread.start();
System.out.println("我是主线程,代码执行结束");
}
}
5、线程的通信
方法
方法名 | 作用 |
---|---|
wait() | 暂停活动在当前对象的线程 |
notify() | 唤醒正在排队等待同步资源的线程中优先级最高者结束等待 |
notifyAll () | 唤醒正在排队等待资源的所有线程结束等待 |
注意:
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则java.lang.IllegalMonitorStateException异常
方法解读
wait()方法
1、在当前线程中调用方法: 对象名.wait()
2、使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止
3、调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
4、调用此方法后,当前线程将释放对象监控权 ,然后进入等待
5、在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行
notify()/notifyAll()
1、在当前线程中调用方法: 对象名.notify()
2、功能:唤醒等待该对象监控权的一个/所有线程
3、调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
经典案例
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品
class Clerk { // 售货员
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e)
{ e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了
第" + product + "个产品");
notifyAll();
} }
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" +
product + "个产品");
product--;
notifyAll();
}} }
class Productor implements Runnable { // 生产者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk; }
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) { e.printStackTrace();
}
clerk.addProduct();
} } }
class Consumer implements Runnable { // 消费者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk; }
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
} } }
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
} }
Join/Wait与sleep之间的区别
1、sleep(long)方法在睡眠时不释放对象锁
2、join(long)方法先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait封装的
3、Wait(long)方法在等待的过程中释放对象锁
6、JDK5.0新增线程创建方式
使用Callable和Future创建线程
Callable和Future 线程可以获取到返回结果 底层基于LockSupport
优点
可以获取到返回结果
缺点
线程异步执行,比较耗时间
package com.nnfx.threads;
import java.util.concurrent.Callable;
/**
* @author 杰仔正在努力
* @create 2022-12-10 15:45
* 使用Callable和Future创建线程
*/
public class ThreadCallable implements Callable<Integer> {
/**
* 当前线程需要执行的代码 返回结果
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "开始执行。。。");
try {
Thread.sleep(10000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "返回结果是1");
return 1;
}
}
package com.nnfx.threads;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author 杰仔正在努力
* @create 2022-12-10 15:49
*/
public class Thread02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCallable threadCallable = new ThreadCallable();
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(threadCallable);
new Thread(integerFutureTask).start();
Integer result = integerFutureTask.get();
System.out.println(Thread.currentThread().getName() + "," + result);
}
}
使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
优点:
1、提高响应速度(减少了创建新线程的时间)
2、降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3、便于线程管理
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ">我是子线程<");
}
});