Java多线程技术
Java多线程技术
Java中如何实现多线程
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
通过继承Thread类实现多线程
继承Thread类实现多线程的步骤:
- 继承Thread类
- 重写方法run( )来完成其操作的,方法run( )为线程体
- 创建Thread对象
- 通过调用Thread类的start()方法来启动一个线程
示例:
public class TestThread extends Thread {//自定义类继承Thread类
//run()方法里是线程体
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
}
}
public static void main(String[] args) {
TestThread thread1 = new TestThread();//创建线程对象
thread1.start();//启动线程
TestThread thread2 = new TestThread();
thread2.start();
}
}
此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。
多实现,少继承。
通过实现Runnable接口实现多线程
在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了继承Thread 类实现多线程的缺点,即在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。
通过实现Runnable接口实现多线程:
- 实现Runnable接口
- 重写run()方法
- 创建实现类对象及Thread对象
- 通过调用Thread类的start()方法来启动一个线程
示例:
public class TestThread2 implements Runnable {//自定义类实现Runnable接口;
//run()方法里是线程体;
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
//创建线程对象,把实现了Runnable接口的对象作为参数传入;
Thread thread1 = new Thread(new TestThread2());
thread1.start();//启动线程;
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}
静态代理设计模式
是装饰器模式的一种,由真实角色和代理角色,可以实现更多功能。
示例:
public class StaticProxy{
public static void main(String[] args) {
new WeddingCompany(new You()).happyMarry();
//多线程实现就是这种模式
//new Thread(线程对象).start();
}
}
interface Marry{
void happyMarry();
}
class You implements Marry{//真实角色
public void happyMarry{
System.out.println("mmmmmm");
}
}
class WeddingCompany implements Marry{//代理角色
private Marry target;
public WeddingCompany(Marry targe){
this.target= target;
}
public void happyMarry() {
ready();
this.target.happyMarry();
after();
}
private void ready(){//后期可以搞一些内存、时间统计、日志之类的辅助功能
System.out.println("gggggg");
}
private void after(){
System.out.println("kkkkkkk");
}
}
线程状态

终止线程的典型方式
终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。
public class TestThreadCiycle implements Runnable {
String name;
boolean live = true;// 标记变量,表示线程是否可中止;
public TestThreadCiycle(String name) {
super();
this.name = name;
}
public void run() {
int i = 0;
//当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
while (live) {
System.out.println(name + (i++));
}
}
public void terminate() {
live = false;
}
public static void main(String[] args) {
TestThreadCiycle ttc = new TestThreadCiycle("线程A:");
Thread t1 = new Thread(ttc);// 新生状态
t1.start();// 就绪状态
for (int i = 0; i < 100; i++) {
System.out.println("主线程" + i);
}
ttc.terminate();
System.out.println("ttc stop!");
}
}
暂停线程执行
暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:
- sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
- yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
sleep()
使线程进入阻塞状态,设定时间到达后再进进入就绪状态。
public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用继承方式实现多线程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(2000);//调用线程的sleep()方法;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
yield()
礼让线程,让当前正在执行线程暂停不是阻塞线程,而是将线程从运行状态转入就绪状态,让cpu调度器重新调度。
public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用继承方式实现多线程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
Thread.yield();//调用线程的yield()方法;
}
}
}
线程的联合
线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。
public class TestThreadState {
public static void main(String[] args) {
System.out.println("爸爸和儿子买烟故事");
Thread father = new Thread(new FatherThread());
father.start();
}
}
class FatherThread implements Runnable {
public void run() {
System.out.println("爸爸想抽烟,发现烟抽完了");
System.out.println("爸爸让儿子去买包红塔山");
Thread son = new Thread(new SonThread());
son.start();
System.out.println("爸爸等儿子买烟回来");
try {
son.join();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("爸爸出门去找儿子跑哪去了");
// 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
System.exit(1);
}
System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");
}
}
class SonThread implements Runnable {
public void run() {
System.out.println("儿子出门去买烟");
System.out.println("儿子买烟需要10分钟");
try {
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "分钟");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("儿子买烟回来了");
}
}
线程的优先级
处于就绪状态的线程,会进入“就绪队列”等待JVM按优先级来挑选。线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。使用下列方法获得或设置线程对象的优先级。
- int getPriority();
- void setPriority(int newPriority);
示例:
public class TestThread {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "t1");
Thread t2 = new Thread(new MyThread(), "t2");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。
获取线程基本信息
- isAlive():判断线程是否还“活”着,即线程是否还未终止
- getPriority():获得线程的优先级数值
- setPriority():设置线程的优先级数值
- setName():给线程一个名字
- getName():取得线程的名字
- currentThread():取得当前正在运行的线程对象,也就是取得自己本身
守护线程
线程分为用户线程和守护线程;虚拟机必须确保用户线程执行完毕;虚拟机不用等待守护线程执行完毕;如后台记录操作日志、监控内存使用等。
使用方式:
thread.setDaemon(true);
线程同步
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和synchronized 块。
synchronized 方法
synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
public synchronized void accessVal(int newVal);
synchronized 块
若将一个大的方法声明为synchronized 将会大大影响效率。synchronized 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。
synchronized(syncObject){
//允许访问控制的代码
}
示例:
public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100, "高");
Drawing draw1 = new Drawing(80, a1);
Drawing draw2 = new Drawing(80, a1);
draw1.start(); // 你取钱
draw2.start(); // 你老婆取钱
}
}
/*
* 简单表示银行账户
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模拟提款操作
*/
class Drawing extends Thread {
int drawingNum; // 取多少钱
Account account; // 要取钱的账户
int expenseTotal; // 总共取的钱数
public Drawing(int drawingNum, Account account) {
super();
this.drawingNum = drawingNum;
this.account = account;
}
@Override
public void run() {
draw();
}
void draw() {
synchronized (account) {
if (account.money - drawingNum < 0) {
System.out.println(this.getName() + "取款,余额不足!");
return;
}
try {
Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingNum;
expenseTotal += drawingNum;
}
System.out.println(this.getName() + "--账户余额:" + account.money);
System.out.println(this.getName() + "--总共取了:" + expenseTotal);
}
}
死锁及其解决方案
死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。
线程并发协作
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
- 生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
- 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
- 缓冲区: 消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。缓冲区是实现并发的核心,缓冲区的设置有3个好处:
- 实现线程的并发协作:有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
- 解耦了生产者和消费者:生产者不需要和消费者直接打交道。
- 解决忙闲不均,提高效率:生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
总的来说,线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:
- 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
- 在生产者消费者问题中,仅有synchronized是不够的。它不能用来实现不同线程之间的消息传递(通信)。
- java.lang.Object类的消息传递(通信)方法
- final void wait():表示线程一直等待,直到得到其它线程通知
- void wait(long timeout):线程等待指定秒参数的时间
- final void wait(long timeout,int nan os):线程等待指定毫秒、微秒的时间
- final void notify():唤醒一个处于等待状态的线程
- final void notifyAll(): 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行
任务定时调度
通过Timer和Timetask,我们可以实现定时启动某个线程。
java.util.Timer
在这种实现方式中,Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。
java.util.TimerTask
TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。
在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
示例:
public class TestTimer {
public static void main(String[] args) {
Timer t1 = new Timer();//定义计时器;
MyTask task1 = new MyTask();//定义任务;
t1.schedule(task1,3000); //3秒后执行;
//t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!
//GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57);
//t1.schedule(task1,calendar1.getTime()); //指定时间定时执行;
}
}
class MyTask extends TimerTask {//自定义线程类继承TimerTask类;
public void run() {
for(int i=0;i<10;i++){
System.out.println("任务1:"+i);
}
}
}
高级主题
QUARTZ框架
实际开发中,我们可以使用开源框架quanz(可在官网下载),更加方便的实现任务定时调度。它分为四个部件:
- Scheduler:调度器,控制所有调度。
- Trigger:触发器,采用DSL模式。
- Job Detail:需要处理的Job。
- Job:执行逻辑。
示例:
public class SimpleExample {
public void run() throws Exception {
// 1.创建Scheduler工厂
SchedulerFactory sf = new StdSchedulerFactory();
// 获取调度器
Scheduler sched = sf.getScheduler();
// 时间
Date runTime = evenMinuteDate(new Date());
// 创建jobdetail
JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();
// 触发器
Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
// 注册任务和触发条件
sched.scheduleJob(job, trigger);
// 启动
sched.start();
try {
// wait 65 seconds to show job
Thread.sleep(65L * 1000L);
// executing...
} catch (Exception e) {
//
}
//关闭
sched.shutdown(true);
}
public static void main(String[] args) throws Exception {
SimpleExample example = new SimpleExample();
example.run();
}
}
HappenBefore
执行代码的顺序可能与编写代码不一致,即虚拟机优化代码顺序,则为指令重排happen—before即:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
- 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱一一即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行一—以尽可能充分地利用CPU。
- 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存,速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。
重排时会遵循数据依赖。
示例:
public class HappenBefore {
private static int a = 0;
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{//线程1更改数据
a=1;
flag=true;
});
Thread t2 = new Thread(()->{//线程2读取数据
if(flag){
a*=1;
}
if(a == 0) {//在此处有一定概率发生HappenBefore
System.out.println("happen before a->"+a);
}
});
t1.start();
t2.start();
//保证t1先执行
t1.join();
t2.join();
}
}
//有时会输出 happen before a->1
volatile
volatile保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:
- 线程对变量进行修改之后,要立刻回写到主内存。
- 线程对变量读取的时候,要从主内存中读,而不是缓存。
各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率。
volatile是不错的机制,用于保证数据同步,即可见性,但是volatile不能保证原子性。
示例:
public class Volatile {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(num == 0){
//此处不写代码,让CPU陷入循环
}
}).start();
Thread.sleep(1000);
num=1;
}
}
//CPU没空查看num的变化,会死循环
//可改为private volatile static int num = 0;解决
ThreadLocal
- 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程。
- ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全,常用的方法,就是get/set/initialValue 方法。
- JDK建议ThreadLocal定义为private static
- ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的方法都可以非常方便地访问这些资源。
- Hibernate的Session 工具类HibernateUtil
- 通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。
示例:
public class ThreadLocalTest01 {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<> (){
proctected Integer initialValue(){
return 200;
}
};
public static void main(String[] args) {
//获取值
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal.get());
//设置值
threadLocal.set(99);
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal.get());
new Thread(new MyRun()).start();
}
public static class MyRun implements Runnable{
public void run() {
threadLocal.set((int)(Math.random()*99));
System.out.println(Thread.currentThread().getName()+"--->"+threadLocal.get());
}
}
}
/**输出
*main-->200
*main-->99
*Thread-0-->33
*/
可重入锁
锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,锁释放。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态。
CAS
锁分为两类:
- 悲观锁:synchronized是独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
- 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS:Compare and Swap 比较并交换:
- 乐观锁的实现:
- 有三个值:一个当前内存值V、旧的预期值A、将更新的值B。先获取到内存当中当前的内存值V,再将内存值V和原值A作比较,要是相等就修改为要修改的值B并返回true,否则什么都不做,并返回false;
- CAS是一组原子操作,不会被外部打断:
属于硬件级别的操作(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率比加锁操作高。 - ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
- 在JUC中有实现,不过是底层的可能是C的实现,直接用就完了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了