JavaSE:多线程编程,从并发基础到高级同步机制
·
一、基本概念
进程
- 进程是指运行中的程序。启动一个进程,操作系统就会为该进程分配内存空间
- 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
- 在Windows系统中,可以使用
Esc + Shift + Ctrl打开任务管理器查看进程
线程
- 线程由进程创建,是进程的一个实体
- 一个进程可以拥有多个线程,如同时进行多个下载任务
单线程与多线程
- 单线程:同一个时刻,只允许执行一个线程
- 多线程:同一个时刻,可以执行多个线程,比如:
- 一个QQ进程,可以同时打开多个聊天窗口
- 一个迅雷进程,可以同时下载多个文件
并发与并行
- 并发:同一个时刻,多个任务交替执行,造成一种"貌似同时"的错觉。单核CPU实现的多任务就是并发
- 并行:同一个时刻,多个任务同时执行。多核CPU可以实现并行
获取CPU核心数
public class CpuInfo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
// 获取当前电脑的CPU数量
int cpuNums = runtime.availableProcessors();
System.out.println("CPU核心数: " + cpuNums);
}
}
二、创建线程的两种方式
1. 继承Thread类
public class ThreadDemo {
public static void main(String[] args) {
// 创建Cat对象
Cat cat = new Cat();
cat.start(); // 启动线程
}
}
// 继承Thread类,可以作为线程使用
class Cat extends Thread {
@Override
public void run() { // 重写run方法以实现业务逻辑
while (true) {
System.out.println("喵");
try {
// 休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2. 实现Runnable接口
Runnable接口
- java是单继承的,在某些情况下一个类可能已经继承了某个父类。
这时在用继承Thread类方法来创建线程显然不可能了。 - java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程
public class RunnableDemo {
public static void main(String[] args) {
Dog dog = new Dog();
// 不能直接调用start()
// 创建Thread对象,把dog对象(实现Runnable)放入Thread
Thread thread = new Thread(dog);
thread.start(); // 底层使用代理模式
}
}
// 通过实现Runnable接口创建线程
class Dog implements Runnable {
int i = 0;
@Override
public void run() {
while (true) {
System.out.println("汪" + ++i + " " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (i == 10) {
break;
}
}
}
}
Thread类的代理模式实现原理
// 线程代理类,模拟了一个最简单的Thread类
class ThreadProxy implements Runnable {
private Runnable target = null; // 属性,类型是Runnable
@Override
public void run() {
if (target != null) {
target.run();
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0(); // 这个方法是真正实现多线程的方法
}
private void start0() {
run();
}
}
三、线程机制
线程工作机制

可以使用jconsole工具监视管理控制台 local终端输入jconsloe回车即可
- 当main线程启动一个子线程Thread-0,主线程不会阻塞,会继续执行
- 这时主线程和子线程交替执行
- 主线程结束若子线程仍在进行,不会造成进程结束
start()与run()的区别
- start():启动线程,会创建新的线程,调用run()方法
- run():直接调用run方法,将会用主线程执行,没有开子线程,造成阻塞,串行化

start()源码分析
public synchronized void start() {
start0();
}
// start0()是本地方法,由JVM调用,底层是C/C++实现
// 真正实现多线程效果的是start0(),而不是run()
private native void start0();
四、线程终止与同步

多线程存在问题
线程终止
- 当线程完成任务后,会自动退出。
- 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式
// 通过通知方式终止线程
public class ThreadStop {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
System.out.println("main线程休眠10s...");
Thread.sleep(10 * 1000);
t1.setLoop(false); // 通知t1线程退出
}
}
class MyThread extends Thread {
private boolean loop = true;
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while (loop) {
// 业务逻辑代码
}
}
}
五、线程同步机制
synchronized关键字
- 问题:多线程编程时,一些敏感数据不允许被多个线程同时访问
- 解决方案:使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性
- 原理:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作
同步代码块
synchronized(对象) {
// 得到对象的锁,才能操作同步代码
// 需要被同步的代码
}
同步方法
synchronized还可以放在方法声明中,表示整个方法为同步方法
public synchronized void method(String name) {
// 需要被同步的代码
}
多窗口售票问题示例
public class SellTicket {
public static void main(String[] args) {
SellTicketTask task = new SellTicketTask();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
t1.start();
t2.start();
t3.start();
}
}
// 使用synchronized实现线程同步
class SellTicketTask implements Runnable {
private static int ticketNum = 100;
private boolean loop = true;
// 同步方法
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + "窗口售尽");
loop = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() +
"卖出一张票,剩余票数" + --ticketNum);
}
// 使用同步代码块的方式
public void sell2() {
synchronized (this) { // 互斥锁在this对象上
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + "窗口售尽");
loop = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() +
"卖出一张票,剩余票数" + --ticketNum);
}
}
@Override
public void run() {
while (loop) {
sell(); // 或使用sell2()
}
}
}

六、互斥锁原理
互斥锁特性
- Java语言中引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为"互斥锁"的标记,保证在任一时刻,只能有一个线程访问该对象
- 关键字synchronized与对象的互斥锁联系。当某个对象被synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:可能导致程序的执行效率降低
锁对象的选择
- 同步方法(非静态):锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态):锁为当前类本身
- 同步方法如果没有使用static修饰:默认锁对象为this
- 如果方法使用static修饰:默认锁对象为当前类.class
实现的落地步骤:
- 先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可!
静态同步方法示例
class SellTicket03 implements Runnable {
// 锁加在SellTicket03.class上
public synchronized static void m1() {
// 同步代码
}
// 静态方法中的同步代码块
public static void m2() {
synchronized (SellTicket03.class) {
System.out.println("m2");
}
}
@Override
public void run() {
// 线程执行逻辑
}
}
七、死锁与释放锁
死锁示例
public class DeadLock {
public static void main(String[] args) {
DeadLockDemo deadLock1 = new DeadLockDemo(true);
DeadLockDemo deadLock2 = new DeadLockDemo(false);
deadLock1.start();
deadLock2.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
// 下面业务逻辑的分析:
// 1. 如果flag为true,线程A就会先得到/持有o1对象锁,然后尝试去获取o2对象锁
// 2. 如果线程A得不到o2对象锁,就会Blocked
// 3. 如果flag为false,线程B就会先得到/持有o2对象锁,然后尝试去获取o1对象锁
// 4. 如果线程B得不到o1对象锁,就会Blocked
if (flag) {
synchronized (o1) { // 对象互斥锁
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
释放锁的情况
会释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 案例:上厕所,完事出来
- 当前线程在同步代码块、同步方法中遇到break、return
- 案例:没有正常的完事,经理叫他修改bug,不得已出来
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 案例:没有正常的完事,发现忘带纸,不得已出来
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停并释放锁
- 案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去
不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行不会释放锁
- 案例:上厕所,太困了,在坑位上眯了一会
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
- 提示:应尽量避免使用suspend()和resume()来控制线程,不推荐使用
八、线程常用方法
核心方法
- setName():设置线程名称,使之与参数name相同
- getName():返回该线程的名称
- start():使该线程开始执行(底层会创建新的线程,调用run())
- run():线程的执行体(直接调用run()不会启动新线程)
- setPriority():更改线程的优先级(范围1-10)
- getPriority():获取线程的优先级
- sleep():线程的静态方法,使当前线程休眠指定的毫秒数
- interrupt():中断线程,但并没有真正的结束线程。一般用于中断正在休眠的线程
- yield():线程的让步,让出CPU,让其他线程执行。礼让的时间不确定,不一定礼让成功
- join():线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
1.setName//设置线程名称,使之与参数name相同
2.getName//返回该线程的名称
3.start//使该线程开始执行:Java虚拟机底层调用该线程的start0方法
4.run //调用线程对象 run方法;
5.setPriority//更改线程的优先级
6.getPriority//获取线程的优先级
7.sleep//在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
8.interrupt//中断线程
守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通过通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
// 设置守护线程
thread.setDaemon(true);
九、线程的七种状态
线程状态详解
- NEW:尚未启动的线程处于此状态
- RUNNABLE:在Java虚拟机中执行的线程处于此状态(包含READY与RUNNING两种子状态)
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态
- WAITING:正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED:已退出的线程处于此状态
线程状态转换图
NEW
↓
start()
↓
RUNNABLE ←→ BLOCKED(等待锁)
↓ ↓
TERMINATED WAITING/TIMED_WAITING
十、线程监控工具
JConsole使用
- 可以监视Java应用程序的线程状态
- 在终端输入
jconsole回车即可启动 - 可以查看线程状态、死锁检测、内存使用等情况
线程同步的最佳实践
- 分析上锁的代码:确定哪些代码需要同步
- 选择同步方式:同步代码块或同步方法
- 确保锁对象一致:要求多个线程的锁对象为同一个
- 尽量减小同步范围:只同步必要的代码,提高效率
- 避免死锁:注意锁的获取顺序
通过合理使用多线程技术,可以充分利用CPU资源,提高程序性能,但需要注意线程安全、死锁等问题,确保程序的稳定性和正确性。
更多推荐




所有评论(0)