JAVA多线程
JAVA多线程
1.0 线程创建
java 通过java.lang.Thread类来表示线程、
下面是实现多线程的方法之一,通过继承Thread来实现
package com.test.thread;
public class MyThread {
public static void main(String[] args) {
Thread t=new MThread();
t.start();//启动线程
for(int i=1 ;i<=4;i++){
System.out.println("主线程运行");
}
}
}
class MThread extends Thread{
//通过run来声明线程需要干的事情
@Override
public void run() {
for(int i=1 ;i<=4;i++){
System.out.println("子线程运行");
}
}
}
输出为:
主线程运行
主线程运行
子线程运行
子线程运行
子线程运行
子线程运行
主线程运行
主线程运行
由于主线程和子线程同时进行,所以执行顺序具有随机性
线程的启动需要start函数,如果直接调用run,cpu没有注册新线程,则会直接执行run,而不是按多线程同时进行
该方法简单,但由于java是单继承的,所以无法继承其他类,不利于拓展
接下来介绍第二种方式
package com.test.thread;
public class MyThread {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.start();
for(int i=1;i<=4;i++) {
System.out.println("主线程运行");
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=1;i<=4;i++){
System.out.println("子线程运行");
}
}
}
第二种创建方式我们通过新建一个继承Runnable 接口的类,重写了run方法,但我们新的类并不是线程类,
我们创建好对象之后需要将其传给线程类构造器将其转换成线程对象
既然我们新建的继承于Runnable的类实现的是一个函数式接口,那直接使用lambda表达式即可
package com.test.thread;
public class MyThread {
public static void main(String[] args) {
new Thread(()-> {
for (int i = 0; i <4 ; i++) {
System.out.println("子线程运行");
}
} ).start();
for(int i=1;i<=4;i++) {
System.out.println("主线程运行");
}
}
}
但上面这几种方法线程运行时都无法返回值,接下来看第三种方法
package com.test.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyThread {
public static void main(String[] args) {
Callable<String> callabll=new MyCallable(5);
FutureTask<String> futureTask=new FutureTask<String>(callabll);
Thread t=new Thread(futureTask);
t.start();
for(int i=1;i<=4;i++) {
System.out.println("主线程运行");
}
try {
System.out.println(futureTask.get());
}
catch (Exception e){
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n){
this.n=n;
}
@Override
public String call() throws Exception {
int sum=0;
for(int i=1;i<=n;i++){
sum+=i;
}
return ""+sum;
}
}
第三种方法我们使用一个继承自Callable的类,通过重写构造方法来实现传入值,但需要注意,start调用call方法
时因为不传参,所以需要通过构造方法设置自身属性方式来实现参数传入我们创建这个新类的对象后将其传给未来
任务类,最后再传给线程类得到线程对象。
最后我们通过调用未来任务对象的get方法获得返回的结果,由于我们获得返回结果的操作是在主线程中,所以调
取返回结果时如果子线程没有执行完毕,则会等待至子线程执行完成后继续主线程取结果
1.1 线程的常用方法
首先来介绍给线程命名的方法
如果现在同时有多个子线程在跑,如何在输出中区分这些子线程呢?此时就需要用到这些方法
package com.test.thread;
public class MyThread {
public static void main(String[] args) {
Thread t1=new MThread("一号线程");
Thread t2=new MThread("二号线程");
//t1.setName("一号线程");//给线程设置名字
t1.start();//启动线程
t2.start();
System.out.println(t1.getName());
for(int i=1 ;i<=4;i++){
System.out.println("主线程运行");
}
System.out.println(t1.getName());//使用getname方法获得子线程的名字
Thread main= Thread.currentThread();//通过静态方法获得当前线程对象
System.out.println(main.getName());//由于当前是在主线程中使用所以获得主线程的名字
}
}
class MThread extends Thread{
//通过run来声明线程需要干的事情
public MThread(String name){
super(name);
}
@Override
public void run() {
for(int i=1 ;i<=4;i++){
System.out.println(Thread.currentThread().getName()+"子线程运行");
}
}
}
我们封装Runnable对象时,也可也设置线程的名字,格式为
Thread(Runnable对象,线程名字)
在线程中可以设置线程休眠
Thread.sleep(休眠时间)
我们传入值是以毫秒为单位
package com.test.thread;
public class MyThread {
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
for (int i = 0; i <4 ; i++) {
System.out.println("子线程运行");
}
} ).start();
for(int i=1;i<=4;i++) {
Thread.sleep(1000);
System.out.println("主线程运行");
}
}
}
接下来看join线程 ,我们已知当两个线程同时运行时,先后执行的顺序是没有规律的,我们使用join方法可以在某一线程执行到一条件被触发时,将另一线程加入到该线程的前面,当加入的线程执行完后才继续执行原来的线程
下面是例子
package com.test.thread;
public class MyThread {
public static void main(String[] args) throws Exception {
Thread t=new MThread();
t.start();//启动线程
for(int i=1 ;i<=4;i++){
if(i==2){
t.join();
}
System.out.println(Thread.currentThread().getName()+"主线程运行");
}
}
}
class MThread extends Thread{
//通过run来声明线程需要干的事情
@Override
public void run() {
for(int i=1 ;i<=4;i++){
System.out.println(Thread.currentThread().getName()+"子线程运行");
}
}
}
我们来查看输出:
main主线程运行
Thread-0子线程运行
Thread-0子线程运行
Thread-0子线程运行
Thread-0子线程运行
main主线程运行
main主线程运行
main主线程运行
可以发现当主线程中的循环执行到第二次时,触发了join方法,此时线程t就会被加入到当线程的前列进行执行
1.2 线程安全
当出现满足下面这几个条件的情况,就会出现线程安全问题条件分别是:
1 存在多个线程同时执行
2 同时访问一个共享资源
3 存在修改该共享资源
现在来看一个触发线程安全的例子:
package com.test.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class MyThread {
public static void main(String[] args) throws Exception {
Account account =new Account("111",100000);
Thread t1=new MThread("用户1",account);
Thread t2=new MThread("用户2",account);
t1.start();
t2.start();
}
}
class MThread extends Thread{
private Account acc;
MThread (String name,Account acc ){
super(name);
this.acc=acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Account{
private String card;
private double money;
public void drawMoney(double money){
String name =Thread.currentThread().getName();
if(this.money>=money){
System.out.println(name+"取钱成功,吐出"+money);
this.money-=money;
System.out.println(name+"剩余钱数为"+this.money);
}
else{
System.out.println(name+"取钱失败,余额不足 当前余额为"+this.money);
}
}
}
此时结果为:
用户2取钱成功,吐出100000.0
用户1取钱成功,吐出100000.0
用户2剩余钱数为0.0
用户1剩余钱数为-100000.0
很明显,这是不行的
1.3 同步代码块
线程安全问题产生的原因就是多个线程同时访问资源,想要解决这个问题,我们可以利用线程同步,同时只允许一
个线程访问资源。
第一种方法,同步代码块,给关键资源代码上锁:
package com.test.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class MyThread {
public static void main(String[] args) throws Exception {
Account account =new Account("111",100000);
Thread t1=new MThread("用户1",account);
Thread t2=new MThread("用户2",account);
t1.start();
t2.start();
}
}
class MThread extends Thread{
private Account acc;
MThread (String name,Account acc ){
super(name);
this.acc=acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Account{
private String card;
private double money;
public void drawMoney(double money){
String name =Thread.currentThread().getName();
synchronized ("lock") {
if(this.money>=money){
System.out.println(name+"取钱成功,吐出"+money);
this.money-=money;
System.out.println(name+"剩余钱数为"+this.money);
}
else{
System.out.println(name+"取钱失败,余额不足 当前余额为"+this.money);
}
}
}
}
对于关键的资源访问代码,使用synchronized 来添加锁,但当前所住的代码区域每次只能允许一个线程访问,但
这样也存在问题,同时只有一个线程访问很明显是没有体现多线程的特点的,这是因为我们使用了唯一的常量字符
作为锁对象,对于不同的线程都是相同的,所以把所有的线程都给锁住了,我们需要的只是将同一个账户的线程上
锁,所以将代表同一个账户账户对象作为锁对象传入,即可解决这个问题:
package com.test.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class MyThread {
public static void main(String[] args) throws Exception {
Account account =new Account("111",100000);
Thread t1=new MThread("用户1",account);
Thread t2=new MThread("用户2",account);
t1.start();
t2.start();
}
}
class MThread extends Thread{
private Account acc;
MThread (String name,Account acc ){
super(name);
this.acc=acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Account{
private String card;
private double money;
public void drawMoney(double money){
String name =Thread.currentThread().getName();
synchronized (this) {
if(this.money>=money){
System.out.println(name+"取钱成功,吐出"+money);
this.money-=money;
System.out.println(name+"剩余钱数为"+this.money);
}
else{
System.out.println(name+"取钱失败,余额不足 当前余额为"+this.money);
}
}
}
}
规范是 静态方法用类名.class 作为锁,实例方法用对象作为锁
1.4 同步方法
package com.test.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class MyThread {
public static void main(String[] args) throws Exception {
Account account =new Account("111",100000);
Thread t1=new MThread("用户1",account);
Thread t2=new MThread("用户2",account);
t1.start();
t2.start();
}
}
class MThread extends Thread{
private Account acc;
MThread (String name,Account acc ){
super(name);
this.acc=acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Account{
private String card;
private double money;
public synchronized void drawMoney(double money){
String name =Thread.currentThread().getName();
if(this.money>=money){
System.out.println(name+"取钱成功,吐出"+money);
this.money-=money;
System.out.println(name+"剩余钱数为"+this.money);
}
else{
System.out.println(name+"取钱失败,余额不足 当前余额为"+this.money);
}
}
}
使用同步方法只需要在资源调用函数前加关键字 synchronized 进行修饰即可,其内部还是使用的同步代码块,
遵循规范是 静态方法用类名.class 作为锁,实例方法用对象作为锁 但由于锁的范围更大,所以性能略差一点,
不过可读性更强
1.5 LOCK锁
使用lock锁,定义好锁对象后,在需要上锁的地方调用lock方法即可上锁,在需要解锁的地方调用unlock方法即可
解锁,锁对象一般定义在对象中,作为对象的属性。下面是例子
package com.test.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread {
public static void main(String[] args) throws Exception {
Account account =new Account("111",100000);
Thread t1=new MThread("用户1",account);
Thread t2=new MThread("用户2",account);
t1.start();
t2.start();
}
}
class MThread extends Thread{
private Account acc;
MThread (String name,Account acc ){
super(name);
this.acc=acc;
}
@Override
public void run() {
acc.drawMoney(100000);
}
}
@Data
@NoArgsConstructor
class Account{
private final Lock lock=new ReentrantLock();
private String card;
private double money;
Account(String card,int money){
this.card=card;
this.money=money;
}
public void drawMoney(double money){
String name =Thread.currentThread().getName();
try {
lock.lock();
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money);
this.money -= money;
System.out.println(name + "剩余钱数为" + this.money);
} else {
System.out.println(name + "取钱失败,余额不足 当前余额为" + this.money);
}
}
catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
1.6 线程通信
当多个线程共同操作共享资源时,线程通过某种方式进行通信,进行协调,避免无效资源竞争
线程通信用到的函数主要有下面这几个:
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用void notify()或者void notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等到的所有线程
1.7线程池和线程复用
我们不可能有多少个任务就创建多少个线程,如果遇到大量任务的情况就会耗尽系统资源,所以要利用线程池实现
线程复用
创建线程池对象:
new ThreadPoolExecutor(
3, // corePoolSize - 核心线程数
5, // maximumPoolSize - 最大线程数
1, TimeUnit.MINUTES, // keepAliveTime + unit - 空闲线程存活时间
new ArrayBlockingQueue<>(3), // workQueue - 任务队列
Executors.defaultThreadFactory(), // threadFactory - 线程工厂
new ThreadPoolExecutor.AbortPolicy() // handler - 拒绝策略
);
对参数进行解释:
corePoolSize = 3 - 核心线程数,始终保持3个线程存活
maximumPoolSize = 5 - 最大线程数,包含核心线程,最多5个线程
keepAliveTime = 1 - 临时线程空闲1分钟后被回收
unit = TimeUnit.MINUTES - 时间单位为分钟
workQueue = ArrayBlockingQueue<>(3) - 有界队列,容量为3
threadFactory - 使用默认线程工厂创建线程
handler = AbortPolicy - 拒绝策略:队列满且达到最大线程数时抛出异常
临时线程的产生条件:核心线程数已满(3个核心线程都在工作)
队列已满(ArrayBlockingQueue 中已有3个任务在等待)
还有新任务提交时
线程池的运行逻辑是当,有新的任务出现被添加进任务队列时,检测是否有空闲线程,如果有空闲线程,则出队列
由线程执行其任务,如果当时没有空线程,就在任务队列中进行等待线程被释放,如果此时任务队列满了,则会产
生临时线程来处理,如果可以产生的临时线程都产生了,此时还有任务需要加入到任务队列中,则会触发拒绝策略
下面介绍几种常见的拒绝策略:
在 ThreadPoolExecutor 中有4种内置的拒绝策略可以选择:
- AbortPolicy(默认策略)
new ThreadPoolExecutor.AbortPolicy()
- 行为:直接抛出
RejectedExecutionException异常 - 适用场景:希望明确知道任务被拒绝的情况
- CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy()
- 行为:由调用线程(通常是主线程)直接执行被拒绝的任务
- 适用场景:不希望丢失任务,可以接受调用线程阻塞
- DiscardPolicy
new ThreadPoolExecutor.DiscardPolicy()
- 行为:静默丢弃被拒绝的任务,不抛异常
- 适用场景:任务丢失可以接受,不需要异常通知
- DiscardOldestPolicy
new ThreadPoolExecutor.DiscardOldestPolicy()
- 行为:丢弃队列中最老的任务,然后尝试重新提交当前任务
- 适用场景:新任务比旧任务更重要
自定义拒绝策略
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑
System.out.println("任务被拒绝:" + r);
}
}
下面给出线程池处理Runnable任务的代码:
package com.test.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor; // 添加这个导入
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executors;
public class MyThreadPoolExecutor {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
1,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable target=new MyRunnable();
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.shutdown();//任务执行完毕再关闭
//pool.shutdownNow();//主线程执行到这立即关闭
}
}
package com.test.thread;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <=2; i++) {
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
System.out.println(Thread.currentThread().getName()+"线程进行休眠");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
接下来我们来学习线程池处理Callable任务
package com.test.thread;
import java.util.concurrent.*;
public class MyThreadPoolExecutor {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
1,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Future<String> f1=pool.submit(new MyCallable(4));
Future<String> f2=pool.submit(new MyCallable(3));
try {
String s1=f1.get();
String s2=f2.get();
System.out.println(s1);
System.out.println(s2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
pool.shutdown();//任务执行完毕再关闭
//pool.shutdownNow();//主线程执行到这立即关闭
}
}
package com.test.thread;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String>{
MyCallable(int n){
this.n=n;
}
private int n;
@Override
public String call() throws Exception {
int ans=0;
for (int i = 0; i <n ; i++) {
ans+=i;
}
return Thread.currentThread().getName()+"当前得到答案"+ans;
}
}
此时调用的是submit 传入MyCallable对象
需要注意,当我们调用String s1 = f1.get(); 主线程会阻塞,等到需要用到的子线程执行完毕才会释放主线程
此时的任务不能复用,我们需要一个任务就需要创建一个MyCallable 不能像Runnable那样多次复用了。
1.8 线程池工具类
我们除了上面的创建线程池方法,还可以使用线程池工具类创建
下面是常见线程池方法

下面是使用:
ExecutorService pool=Executors.newFixedThreadPool(3);
这就创建了一个线程数量为3的线程池,没有临时线程,内部就是对我们之前方法的封装
但实际开发过程不会使用,因为使用工具类创建对资源大小没有限制,所以会出现资源耗尽问题
不适合大型并发场景

浙公网安备 33010602011771号