JavaSE--多线程
1.进程和线程
1.1、定义
进程:
是一个应用程序
线程:
是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
例:对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。
1.2、进程和线程关系
例子:
阿里巴巴:进程
马云:阿里巴巴的一个线程
童文红:阿里巴巴的一个线程
京东:进程
强东:京东的一个线程
妹妹:京东的一个线程
进程可以看做是现实生活当中的公司。
线程可以看做是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)
魔兽游戏是一个进程
酷狗音乐是一个进程
这两个进程是独立的,不共享资源。
线程A和线程B呢?
在java语言中:
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,
互不干扰,各自执行各自的,这就是多线程并发。
火车站,可以看做是一个进程。
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
所以多线程并发可以提高效率。
java中之所以有多线程机制,目的就是为了提高程序的处理效率。
1.3、问题1:多线程机制中,main方法结束,程序会结束吗
使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在
压栈弹栈。
一个线程一个栈
1.4、问题2:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。
电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,
人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像
一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在
这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。
1.5、线程start方法和run方法的区别
1.start方法
start()方法的作用是:
启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码在任务完成之后,瞬间就结束了。
start方法的运行原理
2.run方法
2.实现线程的两种方法
java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。
2.1.第一种方式:
编写一个类,直接继承java.lang.Thread,重写run方法。
// 定义线程类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();
例:
package com.javase.Hashtable.Thread;
public class threadTest01 {
public static void main(String[] args) {
//这里main方法,这的代码属于主线程,在主线中运行。
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
//start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码在任务完成之后,瞬间就结束了。
//启动成功的线程会自动调用run方法,并且run方法在分支栈底部(压栈)。
//run方法在分支线的栈底部,main方法在主栈的栈底部。run和main是平级的。
myThread.start();
//这里的代码还是运行在主线程中。
for (int i = 0; i < 1000; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程------" + i);
}
}
}
2.2.第二种方式
编写一个类,实现java.lang.Runnable接口,实现run方法。
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承
其它的类,更灵活。
例:
package com.javase.Hashtable.Thread;
public class ThreadTest02 {
public static void main(String[] args) {
/* //创建一个可运行对象
MyRunnalble r = new MyRunnalble();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);*/
//上述语句可以缩写成
Thread t = new Thread(new MyRunnalble());
//启动线程
t.start();
for (int i = 0; i < 1000; i++){
System.out.println("主线程" + i);
}
}
}
class MyRunnalble implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println("分支线程" + i);
}
}
}
2.3.采用匿名内部类创建线程对象
package com.javase.Hashtable.Thread;
public class ThreadTest03 {
public static void main(String[] args) {
//采用匿名内部类创建线程对象
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 100; i++){
System.out.println("分支线程" + i);
}
}
});
//启动线程
t.start();
for (int i = 0; i < 100; i++){
System.out.println("主线程" + i);
}
}
}
3.线程的声明周期
线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡
4.线程调度与控制
4.1.修改线程名字
void setName(String name)
//改变线程名称,使之与参数 name 相同。
4.2.获取线程名字
String getName()
//返回该线程的名称。
4.3.获取当前线程对象
static Thread currentThread()
// 返回对当前正在执行的线程对象的引用。
例
这个代码在哪获得的就是哪里的线程
Thread t = Thread.currentThread();
返回值t 就是当前线程
4.4.sleep方法
static void sleep(long millis)
//让当前线程进入休眠,进入"阻塞状态",放弃占有CPU时间片,让其他线程使用
例
package com.javase.Hashtable.Thread;
public class ThreadTest04 {
public static void main(String[] args) {
try {
System.out.println("甘雨我老婆");
Thread.sleep(1000 * 5);
System.out.println("我说的");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.5.终止线程的睡眠
void interrupt()
//中断线程的睡眠。
//这种中断睡眠的方式依靠了java的异常处理机制
//运行时会在slepp方法报异常,然后程序继续运行
4.6.终止线程
void stop()
/*这个方法已经过时了。
这种方式缺点:容易丢失数据,因为这种方式是直接将线程杀死。
线程没有保存的数据将会丢失,不建议使用*/
推荐使用 布尔标记方法
package com.javase.Hashtable.Thread;
public class ThreadTest05 {
public static void main(String[] args) {
MyThread02 r = new MyThread02();
Thread t1 = new Thread(r);
t1.getName();
t1.start();
for (int j = 0; j < 10; j++){
System.out.println(Thread.currentThread().getName() + "-------->" + j);
}
//睡眠五秒
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止进程
//想要什么时候终止进程,就什么时候把标记改成false就可以了
r.run = false;
}
}
class MyThread02 implements Runnable {
//打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//return就结束了,你在结束之前还有什么没保存的。
//在这里就可以保存了。
//终止当前线程
return;
}
}
}
}
4.7.常见的线程调度模型
抢占式调度模型:
那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。
均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
有一些编程语言,线程调度模型采用的是这种方式。
4.7.1 java中与线程调度有关的方法
实例方法:
1.设置线程的优先级
void setPriority(int newPriority)
//设置线程的优先级
2.获取线程优先级
int getPriority()
//获取线程优先级
3.线程优先级
线 程 优 先 级 主 要 分 三 种 :
MAX_PRIORITY(最高级); 最高优先级10
MIN_PRIORITY(最低级); 最低优先级1
NOM_PRIORITY(标准)默认; 默认优先级是5
优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)
静态方法:
1.让位方法
static void yield() 让位方法
//暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。
实例方法:
1.合并线程
void join()
//合并线程
//等待该线程终止,再继续执行原本的线程
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}
5.线程安全问题
关于多线程并发环境下,数据的安全问题。
5.1.重要性
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重要)
5.2.数据在多线程并发的环境下会存在安全问题
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
5.3.解决线程安全问题
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
用排队执行解决线程安全问题。这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
5.4.线程同步和线程异步
5.4.1.异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
异步就是并发。
5.4.2.同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
同步就是排队。
5.5.线程同步机制语法(synchronized)
synchronized(){
//线程同步代码块
}
synchronized后面的小括号中传的这个"数据"是很关键的。这个数据必须是多线程共享的数据,才能达到多线程排队。
()中写什么?
看想让哪些线程同步。
假设t1,t2,t3,t4,t5,有五个线程,只希望t1,t2,t3线程排队,t4,t5不需要排队。
一定要在()写一个t1,t2,t3共享的对象。而这个对象对于t4 t5来说不是共享的
例:银行用户取钱
Account类
package com.javase.Hashtable.Threadsafe;
public class Account {
private String actno;
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//线程同步代码块
synchronized(this){
//取款前的余额
double before = this.getBalance();
//取款后的余额
double after = before - money;
//模拟一下网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}
}
AccountThread类
package com.javase.Hashtable.Threadsafe;
public class AccountThread extends Thread {
//两个线程共享一个账户
private Account act;
//通过构造方法传递过来账户对象
public AccountThread(Account act) {
this.act = act;
}
@Override
public void run() {
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对" +act.getActno() + "取款成功,余额" + act.getBalance());
}
}
Test测试类
package com.javase.Hashtable.Threadsafe;
public class AccountThread extends Thread {
//两个线程共享一个账户
private Account act;
//通过构造方法传递过来账户对象
public AccountThread(Account act) {
this.act = act;
}
@Override
public void run() {
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对" +act.getActno() + "取款成功,余额" + act.getBalance());
}
}
5.5.1 在实例方法上使用synchronized
synchronized出现在实例方法上,一定锁的是this。
缺点:
- 这种方法不灵活
- 另一个缺点是:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序执行的效率降低。这种方法不常用。
优点:
- 代码量少,节俭了
- 当共享对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方法。
5.5.2总结:
synchronized有三种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
5.6.Java三大变量线程相关问题
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
-
局部变量永远都不会存在线程安全问题。
-
因为局部变量不共享。(一个线程一个栈。)
-
局部变量在栈中。所以局部变量永远都不会共享。
-
实例变量在堆中,堆只有1个。
-
静态变量在方法区中,方法区只有1个。
-
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
-
局部变量+常量:不会有线程安全问题。
-
成员变量:可能会有线程安全问题。
6.1、如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。
5.7.死锁机制
死锁案例
package com.javase.Hashtable.Threadsafe;
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
//t1和t2两个线程共享o1,o2
Thread t1 = new MyThread01(o1,o2);
Thread t2 = new MyThread02(o1,o2);
t1.start();
t2.start();
}
}
class MyThread01 extends Thread {
Object o1;
Object o2;
public MyThread01(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
}
}
}
}
class MyThread02 extends Thread{
Object o1;
Object o2;
public MyThread02(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run(){
synchronized(o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o1){
}
}
}
}
死锁问题解决
第一种方案:
尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
对象不共享,就没有数据安全问题了。)
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
6.守护线程
1.java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
2.守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据 自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,没到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。
package com.javase.Hashtable.Thread;
public class ThreadTest04 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据");
t.setDaemon(true);
t.start();
//主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "----->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
@Override
public void run() {
int i = 0;
while(true){
System.out.println(Thread.currentThread().getName() + "----->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7.定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
例:每周要进行银行账户的总账操作。每天要进行数据的备份操作。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种方式实现:
- 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
- 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
- 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
8.实现线程的第三种方式
实现Callable接口。(JDK8新特性。)
这种方式实现的线程可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。
优点:可以获取到线程的执行结果
缺点:效率较低,在获取t线程执行结果的时候,当前线程受阻塞。
package com.javase.Hashtable.Thread;
import com.sun.javaws.IconUtil;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest06 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个"未来任务类"对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
//call方法相当于run方法,只不过这个有返回值
//模拟执行
System.out.println("call begin");
Thread.sleep(1000 * 10);
System.out.println("call end");
int a = 100;
int b = 200;
return a + b; // 自动装箱为 Integer
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
// 这里main方法,这是在主线程中
//在主线程中,怎么获取t线程的退回结果?
//get()方法的执行会导致"当前线程阻塞"
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
//main方法这里的程序想要执行必须等待get()方法的结束
//而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
//另一个线程执行是需要时间的
System.out.println("hello world");
}
}
9.Object类中的方法
关于Object类中的wait和notify方法。(生产者和消费者模式!)
第一:wait和notify方法不是线程对象的方法,
是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),也不是这样的:t.notify()..不对。
第二:wait()方法作用
Object o = new Object();
o.wait();
表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让"当前线程(正在o对象上活动的线程)"进入等待状态。
第三:notify()方法作用
Object o = new Object();
o.notify();
表示:唤醒正在o对象上等待的线程。
第四:notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
9.1生产者和消费者模式
1.生产者和消费者的定义:
生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法
2.wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
3.wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。
4.wait方法作用:o.wait()让正在o对象上获取的线程t进入等待状态,并且释放掉之前占有的o对象的锁。
5.notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
代码实例:
模拟需求
仓库采用List集合
List集合中假设只能存储一个元素
1个元素就表示仓库满了
如果List集合中元素个数是0,表示仓库空了。
保证List集合中永远都是最多存储1个元素。
必须做到:生产1个消费1个
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~