多线程(通俗易懂,一篇就够了)

多线程(通俗易懂,一篇就够了)

一.认识线程

1.1概念

1)线程是什么

一个线程就是一个执行流。每个线程之间都可以按照顺序执行自己的代码。多线程之间“同时” 执行着多份代码。

2)为什么要有线程

首先,“并发编程” 成为 “刚需”。

单核CPU的发展遇到瓶颈,想要提高算力,就需要多核CPU。并发编程能充分利用多核CPU资源。

有些任务场景需要“等待IO”,为了让等待IO的时间能够去做一些其他工作,也可用到并发编程

其次,虽然多进程也可以实现并发编程,但是线程比进程更加轻量

“轻量”指的是

创建线程比创建进程更快。

销毁线程比销毁进程更快。

调度线程比调度进程更快。

最后,线程虽然比进程更轻量,但是人们还不满足,于是有了“线程池” 和“协程”。

3)进程和线程的区别

进程是包含线程的。每个进程都至少有一个主线程,即主线程。

进程和进程之间的不共享内存空间,同一个进程的线程之间共享一个内存空间。

进程是系统分配资源的最小单位,线程是系统调度的最小单位。

一个进程挂了一般不会影响到其他进程。但是一个线程挂了,可能把同进程内的其他线程一起带走(整个进程崩溃)

4)java的线程和操作系统线程的关系

线程是操作系统的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(例如Linux的pthread)

java标准库的类Thread类是对操作系统提供的API的进一步抽象和封装。

1.2写个简单程序感受下多线程程序

import java.util.Random;

public class Demo29 {

private static class MYThread extends Thread {

@Override

public void run() {

Random rand = new Random();

while(true) {

System.out.println(Thread.currentThread().getName());

try {

Thread.sleep(rand.nextInt(10));

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

public static void main(String[] args) {

MYThread t1 = new MYThread();

t1.start();

Random random = new Random();

while(true) {

System.out.println(Thread.currentThread().getName());

try {

Thread.sleep(random.nextInt(10));

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

1.3创建线程

方法1 继承Thread类

1. 继承Thread来创建一个线程类

class MyThread extends Thread {

@Override

//这个方法就是线程的入口

public void run() {

while(true) {

System.out.println("hello thread");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

2.创建MyThread类的实例

Thread t = new MyThread();//向上转型

MyThread t1= new MyThread();

3.调用start方法启动线程

t.start();

方法2 实现Runnable接口

1.实现Runnable接口

lass MyRunnable implements Runnable {

@Override

public void run() {

while(true) {

System.out.println("hello thread");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

2.创建Thread类实例,调用Thread的构造方法时Runnable对象作为target参数

Runnable runnable = new MyRunnable();

Thread t = new Thread(runnable);

Thread t2 = new Thread(new MyRunnable());

//两种都可以

3.调用start方法

t.start();

对比上面两种方法:

继承Thread类,直接使用this就表示当前线程对象的引用。

实现Runnable接口,this表示的是MyRunnable的引用,需要使用 Thread.currentThread();

其他变形

匿名内部类创建Thread子类对象

//继承thread,匿名内部类

Thread t = new Thread() {

@Override

public void run() {

while(true) {

System.out.println("hello thread");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

};

匿名内部类创建Runnable子类对象

//实现Runnable (匿名内部类)

Thread t = new Thread(new Runnable() {

@Override

public void run() {

while(true) {

System.out.println("hello thread");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

});

lambda表达式创建Runnable子类对象 (比较常用)

Thread t = new Thread(() -> {

while(true) {

System.out.println("hello thread");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

});

1.4多线程的优势-增加运行速度

private static final long count = 10_0000_0000;

public static void main(String[] args) throws InterruptedException {

//并发执行

concurrency();

//串行执行

serial();

}

private static void concurrency() throws InterruptedException {

long begin = System.nanoTime();

//利用线程计算a

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

int a = 0;

for (int i = 0; i < count; i++) {

a--;

}

}

});

thread.start();

//主线程内计算b的值

int b = 0;

for(long i = 0; i < count; i++) {

b--;

}

thread.join();

//统计耗时

long end = System.nanoTime();

double ms = (end - begin) * 1.0 / 1000 / 1000;

System.out.printf("并发: %f 毫秒%n",ms);

}

private static void serial() {

//全部在主线程内计算a.b的值

long begin = System.nanoTime();

int a = 0;

for (int i = 0; i < count; i++) {

a--;

}

int b = 0;

for (int i = 0; i < count; i++) {

b--;

}

long end = System.nanoTime();

double ms = (end - begin) * 1.0 / 1000 / 1000;

System.out.printf("串行: %f 毫秒%n",ms);

}

执行结果

并发: 265.180500 毫秒

串行: 496.645300 毫秒

二.Thread类及常用方法

2.1Thread的常见构造方法

方法说明Thread()创建线程对象Thread(Runnable target)使用Runnable对象创建线程对象Thread(String name)创建线程对象,并命名Thread(Runnable target ,String name)使用Runnable对象创建对象,并命名Thread(ThreadGroup,Runnable target)线程可以被用来分组管理2.2Thread的几个常见属性

属性获取方法IDgetId()名称getName()状态getState()优先级

getPriority()

是否后台线程

isDaemom()

是否存活isAlive()是否被中断isInterrupted()

ID是线程的唯一标识,不同线程不会重复

名称是各种调试工具用到

状态表示线程当前所处的一个情况,下面我们会进一步说明

优先级高的线程理论上说更容易被调度到

关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行

是否存活,即简单的理解,为run方法是否运行结束了

线程的中断问题,下面我们进一步说明

public static void main(String[] args) {

Thread thread = new Thread(() -> {

for (int i = 0; i < 10; i++) {

try {

System.out.println(Thread.currentThread().getName()+ ":我还");

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println(Thread.currentThread().getName()+"我即将死去");

});

System.out.println(Thread.currentThread().getName()+ ":ID"+thread.getId());

System.out.println(Thread.currentThread().getName()+":名称"+thread.getName());

System.out.println(Thread.currentThread().getName()+":状态"+thread.getState());

System.out.println(Thread.currentThread().getName()+":优先级"+thread.isAlive());

System.out.println(Thread.currentThread().getName()+":后台线程" +thread.isDaemon());

System.out.println(Thread.currentThread().getName()+":活着"+thread.isAlive());

System.out.println(Thread.currentThread().getName()+":被中断"+ thread.isInterrupted());

thread.start();

while(thread.isAlive()) {

System.out.println(Thread.currentThread().getName()+"状态"+ thread.getState());

}

}

2.3启动一个1线程-strat()

之前我们已经看到了如何通过覆写run方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

覆写run方法是提供给线程要做的事情的指令清单,而调用start()方法,是让线程可以行动起来了。

2.4 中断一个线程

目前常见的有两种方式:

1.通过共享的标记来进行沟通

2.调用interrupt()方法来通知

示例-1 使用自定义的变量来作为标志位.

private static volatile boolean isQuit = false;

public static void main(String[] args) throws InterruptedException {

Thread t = new Thread(() -> {

while(!isQuit) {

System.out.println("线程工作中");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

System.out.println("线程工作完毕");

});

t.start();

Thread.sleep(5000);

isQuit = true;

System.out.println("设置isQuit 为true");

}

示例-2 使用Thread.interrupted() 或者 Thread。currentThread().isInterrupt()代替自定义标志位

public static void main(String[] args) {

Thread t = new Thread(() -> {

//thread类内部,有一个现成的标志位,可以用来判定当前的循环是否要结束

while(!Thread.currentThread().isInterrupted()) {

System.out.println("线程工作中");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

break;

}

}

});

t.start();

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("让t线程终止");

t.interrupt();

}

thread收到通知的方式有两种:

1.如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InterruptedException异常的形式通知,清楚中断标志。

当出现InterruptedException的时候,要不要结束线程取决于catch中代码的写法。可以选择忽略这个异常,也可跳出循环结束线程。

2.否则,只是内部的一个中断标志被设置,thread可以通过

Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通过收到的更及时,即使线程正在sleep也可以马上收到。

2.5等待一个线程-join()

有时我们需要等待一个线程完成他的工作之后,才能进行自己的下一步工作。

ublic static void main(String[] args) throws InterruptedException {

Thread t =new Thread(() -> {

for (int i = 0; i < 5; i++) {

System.out.println("线程工作中");

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

});

t.start();

//让主线程来等待t线程执行结束

//一旦调用join,主线程会触发阻塞,此时t线程可以趁机完成工作

//一直阻塞到t执行结束完毕,join才会解除阻塞

System.out.println("等待开始");

t.join();

System.out.println("等待结束");

}

附录

方法说明public void join()等待线程结束public void join(long millis)等待线程结束,最多等millis毫秒public void join(long millis, int nanos)同理,但是更高精度2.6获取当前线程引用

方法说明public static Thread currentThread();返回当前线程对象的引用public static void main(String[] args) {

Thread thread = Thread.currentThread();

System.out.println(thread.getName());

}

2.7休眠当前线程

是我们比较熟悉一组的方法,有一点要记得,因为线程的调度不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

方法说明public static void sleep(long millis) 休眠当前线程多少毫秒public static void sleep(long millis,int nanos)同理,可以更高精度

public static void main(String[] args) throws InterruptedException {

long beg = System.currentTimeMillis();

Thread.sleep(1000);

long end = System.currentTimeMillis();

System.out.println("时间"+(end-beg)+"ms");

}

时间1012ms

三.线程的状态

3.1观察线程的所有状态

线程的状态是一个枚举类型Thread.State

NEW:安排了工作,还未开始行动。

RUNNABLE :可工作的,又可以分成正在工作中和即将开始工作。

BLOCKED:这几个都表示排队等着其他事情

WAITING:这几个都表示排队等着其他事情。

TIMED_WAITING:这几个都表示排队等着其他事情。

TERMINATED:工作完成了。

3.2线程状态和状态转移的意义

3.3观察线程的状态和转移

public static void main(String[] args) throws InterruptedException {

Thread t = new Thread(() -> {

while(true) {

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

});

//在调用start之前获取状态,此时的状态是new

System.out.println(t.getState());

t.start();

for (int i = 0; i < 5; i++) {

System.out.println(t.getState());

Thread.sleep(1000);

}

t.join();

System.out.println(t.getState());

}

结论

BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知。

TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在无线等待唤醒

四.多线程带来的风险-线程安全(重点)

4.1观察线程不安全

private static int count = 0;

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Object locker2 = new Object();

Thread t1 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

synchronized(locker) {

count++;

}

}

});

Thread t2 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

//如果两个锁不一样,也会存在线程安全问题

synchronized(locker2) {

count++;

}

}

});

t1.start();

t2.start();

//如果没有这俩join,肯定不行,线程没有自增完就开始打印了 很可能出来的就是0;

t1.join();

t2.join();

System.out.println("count "+ count);

}

4.2线程安全的概念

如果多线程环境下代码运行的结果是符合我们的预期,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3线程不安全的原因

线程调度是随机的

这是线程安全问题的罪魁祸首

修改共享数据

上述的线程不安全的代码中,涉及到多个线程针对count变量进行修改。

此时这个count是一个多个线程都能访问到的“共享数据”。

原子性

一条Java语句不一定是原子的,也不一定只是一条指令

比如我们熟知的n++,其实是由三步操作组成的:

1.从内存中把数据读到CPU

2.进行数据更新

3.把数据写回CPU

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的

可见性

可见性是指一个线程对共享变量值的修改,能够及时地其他线程看到。

Java内存模型(JVM):Java虚拟机规范中定义Java内存模型。

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

线程之间的共享变量存在 主内存(Main Memory)

每一个线程都有自己的工作内存(Working Memory)

当线程要读取一个共享变量的时候,会先把变量从内存拷贝到工作内存,再从工作内存读取数据

指令重排序

什么是指令重排序

举个例子

你可以选择1->2->3->4的路径,当然也可以选择2->4->3->1的路径。明显可以少走一些路,这个就叫做指令重排序

4.4解决之前的线程不安全问题

private static int count = 0;

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Object locker2 = new Object();

Thread t1 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

synchronized(locker) {

count++;

}

}

});

Thread t2 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

//如果两个锁不一样,也会存在线程安全问题

synchronized(locker) {

count++;

}

}

});

t1.start();

t2.start();

//如果没有这俩join,肯定不行,线程没有自增完就开始打印了 很可能出来的就是0;

t1.join();

t2.join();

System.out.println("count "+ count);

}

五.synchronized关键字 -监视器锁 monitor lock

5.1sychronized的特性

1)互斥

sychronized会起到互斥效果,某个线程执行到某个对象的sychronized中时,其他线程如果也执行到同一个对象sychronized就会阻塞等待

进入sychronized修饰的代码块,相当于加锁

退出sychronized修饰的代码块,相当于解锁

2)可重入

synchronized同步块对同一条线程来说是可重入的

5.3Java标准库中的线程安全类

Java标准库中很多线程都是不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施。

ArrayList

LinkedList

TreeMap

HashSet

TreeSet

StringBuilder

但是还有一些是线程安全的,使用了一些锁机制来控制。

Vector(不推荐使用)

HashTable(不推荐使用)

ConcurrentHashMap

StringBuffer

还有的虽然没有加锁,但是不涉及“修改”,仍然是线程安全的

String

六.volatile关键字

volatile能保证内存可见性

代码在写入volatile修饰的变量的时候,

改变线程工作内存中volatile变量副本的值

将改变后的副本的值从工作内存中刷新到主内存

代码在读取volatile修饰的变量的时候,

从主内存中读取volatile变量的最新值到线程的工作内存中

从工作内存中读取volatile变量的副本

private static volatile int isQuit = 0;

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

while (isQuit == 0) {

}

System.out.println("t1退出");

});

t1.start();

Thread t2 = new Thread(() -> {

System.out.println("请输入isQuit");

Scanner sc = new Scanner(System.in);

isQuit = sc.nextInt();

});

t2.start();

}

volatile不保证原子性

class Counter {

volatile public int count ;

public void increase() {

count++;

}

}

public static void main(String[] args) throws InterruptedException {

Counter counter = new Counter();

Object lock = new Object();

Thread t1 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

counter.increase();

}

});

Thread t2 = new Thread(() -> {

for (int i = 0; i < 50000; i++) {

counter.increase();

}

});

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println(counter.count);

}

把increase方法的synchronized去掉

给count加上volatile关键字

count的记过无法保证是100000

七.wait和notify

由于线程之间是抢占式执行的,因此线程之间的执行的先后顺序难以预测。

但是实际开发中我们更希望合理的协调多个线程之间的执行先后顺序。

7.1wait()方法

wait做的事情:

使当前执行代码的线程进行等待(把线程放到等待队列中)

释放当前的锁

满足一定条件时被唤醒,重新尝试获取这个锁

wait需要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常

wait结束等待的条件

其他线程调用该对象的notify() 方法

wait等待时间超时(wait有一个带时间参数的方法,可以指定等待时间)

其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptionException异常

7.2notify()方法

notify()方法是唤醒等待的线程。

方法notify()也要同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,并使他们重新获取该对象的对象锁。

如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程。(并没有先来后到)

static class WaitTask implements Runnable {

private Object locker;

public WaitTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized (locker) {

while(true) {

try {

System.out.println("wait开始");

locker.wait();

System.out.println("wait结束");

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

}

static class NotifyTask implements Runnable {

private Object locker;

public NotifyTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized (locker) {

System.out.println("notify开始");

locker.notify();

System.out.println("notify结束");

}

}

}

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Thread t1 = new Thread(new WaitTask(locker));

Thread t2 = new Thread(new NotifyTask(locker));

t1.start();

Thread.sleep(6000);

t2.start();

}

7.3notifyAll()方法

notifyAll方法可以一次唤醒所有线程

static class WaitTask implements Runnable {

private Object locker;

public WaitTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized (locker) {

while(true) {

try {

System.out.println("wait开始");

locker.wait();

System.out.println("wait结束");

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

}

static class NotifyTask implements Runnable {

private Object locker;

public NotifyTask(Object locker) {

this.locker = locker;

}

@Override

public void run() {

synchronized (locker) {

System.out.println("notify开始");

locker.notifyAll();

System.out.println("notify结束");

}

}

}

public static void main(String[] args) throws InterruptedException {

Object locker = new Object();

Thread t1 = new Thread(new WaitTask(locker));

Thread t4 = new Thread(new NotifyTask(locker));

Thread t3 = new Thread(new WaitTask(locker));

Thread t2 = new Thread(new WaitTask(locker));

t1.start();

t3.start();

t2.start();

Thread.sleep(1000);

t4.start();

}

增加两个线程,将notify改为notifyAll 发现确实可以唤醒3个线程,但是这三个线程会产生锁竞争。并不是同时执行,而仍然是有先后的执行。

相关推荐