多线程基础
一、线程的概念
1.1 何为进程?
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
通过任务管理器查看Windows中当前运行的进程:
1.2 何为线程?
-
线程与进程相似,但线程是一个比进程更小的执行单位。
-
线程是独立的执行路径。
-
一个进程在其执行的过程中可以产生多个线程。
-
一个进程如果开辟了多个线程,线程的顺序是由线程调度机制安排的,而调度器与操作系统紧密相关。
-
与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-
对同一个资源操作时,会存在资源抢夺的问题,需要加入并发控制。
Java程序自带两个线程,分别是:main()
线程,gc
线程。main()
线程为主线程,是系统的入口,用于执行整个程序。
1.3 进程和线程的区别
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程与进程的区别:
-
定义方面:进程是程序在某个数据集合上的一次运行活动;线程是进程中的一个执行路径。(进程可以创建多个线程)
-
角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是CPU调度的单位。
-
资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
-
独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。
-
开销方面:进程切换的开销较大。线程相对较小。
1.4 为何使用多线程?
- 多线程就是多个线程同时运行或交替运行。
- 为了更好地利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束之后才能进行;如果使用多线程,则在主线程执行任务的同时可以执行其他任务,而不需要等待。
- 使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程,线程之间可以共享数据。
- 多线程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
1.5 多线程带来的问题
- 内存泄漏
- 上下文切换
- 死锁
- 受限于硬件和软件的资源闲置问题
二、重要概念
2.1 同步和异步
-
同步和异步通常用来形容一次方法调用。
-
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
-
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
关于异步目前比较经典以及常用的实现方式就是消息队列:
在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。
但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。
2.2 并发和并行
-
并发(Concurrency)和并行(Parallelism)都表示两个或者多个任务一起执行,但是偏重点有些不同。
-
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。
-
而并行是真正意义上的“同时执行”。
多线程在单核CPU是顺序执行,也就是交替运行(并发)。
多线程在多核CPU中执行,因为每个CPU有自己的运算器,所以多个线程在多个CPU中可以同时运行(并行)。
2.3 高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
2.4 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。
2.5 阻塞和非阻塞
非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,而阻塞与之相反。
三、线程的创建
创建多线程的4种方法:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建
3.1 继承Thread类
- 继承Thread类
- 重写
run()
方法,编写线程执行体 - 创建线程类对象,调用
start()
方法启动线程
例子:
public class MyThread extends Thread { // 继承Thread类
@Override // 重写run()方法
public void run() { // 编写线程执行体
super.run();
for (int i = 0; i < 20; i++) {
System.out.println("myThread:" + i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread(); // 创建线程类对象
myThread.start(); // 调用start()方法启动线程
for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}
}
运行结果:
3.2 实现Runnable接口
- 实现Runnable接口
- 重写
run()
方法,编写线程执行体 - 创建线程类对象,使用Runnable实现类,调用
start()
方法启动线程
例子:
public class RunnableImpl implements Runnable { // 实现Runnable接口
@Override // 重写run()方法
public void run() { // 编写线程执行体
for (int i = 0; i < 20; i++) {
System.out.println("runnableImpl:" + i);
}
}
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread thread = new Thread(runnable); // 创建线程类对象,使用Runnable实现类
thread.start(); // 调用start()方法启动线程
for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}
}
运行结果:
四、线程的状态
4.1 线程停止
- 不推荐使用JDK提供的stop()、destroy()方法
- 推荐让线程自己停下来
- 使用一个标记变量标记线程,当标记变量为false时,中止线程运行。
例子:
public class RunnableImpl implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("thread run" + i++);
}
}
public void stop() {
flag = false;
}
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread thread = new Thread(runnable);
thread.start();
for (int i = 0; i < 200; i++) {
System.out.println("main:" + i);
if (i == 150) {
runnable.stop();
System.out.println("线程该停止了");
}
}
}
}
运行结果:
4.2 线程休眠
sleep(时间)
指定当前线程阻塞的毫秒数sleep()
存在异常InterruptedExceptionsleep()
执行后进入阻塞,时间到达后,线程进入可运行状态sleep()
可以模拟延时网络- 每个对象都有一把锁,sleep()不会释放锁
例子:模拟倒计时
public class ThreadSleep {
public static void main(String[] args) {
try {
CountDown(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void CountDown(int num) throws InterruptedException {
for (int i = num; i > 0; i--) {
System.out.println(i);
Thread.sleep(1000);
}
}
}
4.3 线程礼让
- 让当前正在执行的线程暂停,回到可运行状态,不会阻塞
- 让CPU重新调度,礼让不一定成功,看CPU调度
例子:
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行");
if (Thread.currentThread().getName() == "a") {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "停止执行");
}
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
new Thread(runnable,"a").start();
new Thread(runnable,"b").start();
}
}
运行结果:礼让成功
4.4 线程强制执行
join()
合并线程,指定进程插队开始执行,其他线程进入阻塞状态,待此线程执行完成之后,再执行其他线程- 类似插队
例子:
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
// Lamda表达式
Thread thread = new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("线程vip来啦!" + i);
}
});
thread.start();
for (int i = 0; i < 50; i++) {
if (i == 20) {
thread.join();
}
System.out.println("main" + i);
}
}
}
运行结果:
4.5 观测线程状态
通过thread.getState()
方法查看线程当前状态
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
// Lamda表达式
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.State state = thread.getState();
System.out.println(state); // NEW
thread.start();
state = thread.getState();
System.out.println(state); // RUNNABLE
while (state != Thread.State.TERMINATED) {
Thread.sleep(100);
state = thread.getState();
System.out.println(state);
}
}
}
4.6 守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 守护线程如:后台记录操作日志,监控内存,垃圾回收等待
- 通过
thread.setDaemon(true)
设置为守护线程,false表示用户线程。
4.7 线程优先级
- Java提供了一个线程调度器来监控程序启动后进去就绪状态的所有线程。线程调度器通过线程的优先级来决定调度哪些线程执行。
- 在Java中,线程优先级用1~10来表示,分为三个级别
- 低优先级:1~4,Thread.MIN_PRIORITY=1
- 默认优先级:5,Thread.NORM_PRIORITY=5
- 高优先级:6~10,Thread.MAX_PRIORITY=10
- 使用
getPriority()
或setPriority(int xxx)
,获取和设置优先级 - 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用,还是要看CPU的调度。
例子:
public class PriorityThread implements Runnable {
public static void main(String[] args) {
PriorityThread priorityThread = new PriorityThread();
Thread t1 = new Thread(priorityThread);
Thread t2 = new Thread(priorityThread);
Thread t3 = new Thread(priorityThread);
Thread t4 = new Thread(priorityThread);
Thread t5 = new Thread(priorityThread);
t1.start();
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
t4.setPriority(3);
t4.start();
t5.setPriority(8);
t5.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
}
}
运行结果:
五、线程同步机制
- 由于同一进程的多个线程共享同一块存储空间,会造成访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用资源后释放锁即可。
5.1 同步方法
通过synchronized
关键字修饰,如:
public synchronized void method(int args) {}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才获得这个锁,继续执行。
synchronized方法默认锁住this
缺陷:若将一个大方法声明为synchronized将会影响效率。
例子:
public class TicketStation implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "正在买票:" + ticketNum--);
}
}
5.2 同步块
- 通过
synchronized(Obj){}
包裹 - Obj称为同步监视器,可以是任何对象,但推荐使用共享资源作为同步监视器
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,锁定并访问
例子:
public class TicketStation implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
synchronized (this) {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "正在买票:" + ticketNum--);
}
}
}
5.3 死锁
多个线程互相抱着对方需要的资源,形成僵持。
public class DeadLockTest {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"灰姑凉");
Makeup g2 = new Makeup(1,"白雪公举");
g1.start();
g2.start();
}
}
class Lipstick{
}
class Mirror{
}
class Makeup extends Thread{
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public Makeup(int choice,String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子");
}
}
}else {
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子");
Thread.sleep(2000);
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红");
}
}
}
}
}
5.4 Lock(锁)
- 显示定义同步锁来实现同步,同步锁使用Lock对象充当,每次只能有一个线程对Lock对象加锁。
- ReentrantLock(可重入锁)类实现了Lock接口。
- Lock只有代码块锁。
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try{
lock.lock();
if (ticketNums > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}else {
break;
}
}finally {
lock.unlock();
}
}
}
}