文章目录
前言
提示:这里介绍了多线程的基础知识点,及注意事项
提示:以下是本篇文章正文内容,下面案例可供参考
一、多线程的概述
1、并行与并发:
并行:指两个或多个事件在同一时刻发生(同时执行);
并发:之两个或多个事件在同一时间段内发生(交替执行)
2、进程与线程
进程:进程是程序的一次执行过程,是系统运行程序的基本单位;
1、进程是应用程序的可执行单元
2、一个应用程序可以有多个进程
3、每个进程执行都会有独立的内存空间
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一条线程。
线程是进程的可执行单元
一个进程可以有多条线程
每个线程执行都会有独立的内存空间
3、多线程的注意点:
1、java只有单进程,然后有多线程。
2、一个进程只能执行一条线程,所以java中只有多线程并发,没有多线程并行。
4、线程的调度:
4.1、分时调度:
所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的使用时间。
4.2、抢占式调度:
优先让优先级高的线程使用cpu,如果优先级相同,那么会随机选一个(线程随机性)
注意了注意了!!!
java线程的调度方式是抢占式。
5、线程的创建方式:
5.1、继承的方式(继承Thread类)
案例:
public class MyThread extends Thread {
@Override
public void run() {
// 线程需要执行的任务代码
for (int i = 0; i < 100; i++) {
System.out.println("子线程i的值是:"+i);
}
}
}
public class Test {
public static void main(String[] args) {
/*
- 创建一个子类继承Thread类
- 在子类中重写run方法,把线程需要执行的任务代码放入run方法中
- 创建子类对象,调用start()方法启动线程,执行任务
注意:
1.线程不能重复启动,只能启动一次
2.启动线程,一定要调用start()方法
*/
// 创建线程对象
MyThread mt = new MyThread();
// 启动线程,执行任务
mt.start();// 默认调用run方法
// 线程需要执行的任务代码
for (int j = 0; j < 100; j++) {
System.out.println("主线程j的值是:"+j);
}
}
}
5.2、实现的方式(实现Runnable接口)
案例:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程需要执行的任务代码
for (int i = 0; i < 100; i++) {
System.out.println("子线程i的值是:"+i);
}
}
}
public class Test {
public static void main(String[] args) {
/*
实现步骤:
- 创建实现类实现Runnable接口
- 在实现类中,重写run方法,把线程需要执行的任务代码放入run方法中
- 创建实现类对象
- 创建Thread线程对象,并传入实现类对象
- 使用Thread线程对象调用start方法启动线程,执行任务
*/
// 创建实现类对象
MyRunnable mr = new MyRunnable();
// 创建Thread线程对象
Thread t = new Thread(mr);
// 启动线程执行任务
t.start();
// 主线程需要执行的任务代码
for (int j = 0; j < 100; j++) {
System.out.println("主线程j的值是:"+j);
}
}
}
5.3、匿名内部类的实现方式:
案例:
public class Test {
public static void main(String[] args) {
/*
- 创建Thread线程对象,并传入Runnable接口的匿名内部类
- 在Runnable匿名内部类中重写run方法,书写线程需要执行的任务代码
- 使用Thread线程对象调用start方法启动线程,执行任务
*/
// 创建线程对象,传入任务对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 线程任务需要执行的任务代码
for (int i = 0; i < 100; i++) {
System.out.println("子线程i的值是:"+i);
}
}
});
// 启动线程,执行任务
t.start();
// 主线程需要执行的任务代码
for (int j = 0; j < 100; j++) {
System.out.println("主线程j的值是:"+j);
}
}
}
6、实现方式创建线程的优势:
**
实现Runnable比继承Thread类所具有的优势:
**
1、适合多个相同的程序代码的线程去共享同一个资源(任务)
2、可以避免java中单继承的局限性
3、增加程序的健壮性,实现解耦操作;代码可以被多个线程共享,实现代码和线程独立
4、线程池中只能放入实现Runnable和Callable类线程,不能直接放入继承Thread的类
二、多线程中的线程安全问题
1、高并发及线程安全的原因:
1、高并发:是指在某个时间点上,有许多用户(线程)同时访问统一资源。例如双十一、12306在线抢票,都会面临大量用户同时抢购同一件商品/票的情况。
2、线程安全:当我们使用多个线程访问同一个资源的时候,并且多个线程对该资源有写的操作,可能会导致被访问的资源出现“数据污染”,就容易出现线程安全问题。
2、多线程的运行机制:
2.1、原理:抢占式调度
2.2、特点:
1、当一个线程启动后,JVM会为其分配一个独立的“线程栈区”,这个线程会在这个独立的栈区运行。
2、结论:线程启动了就会在栈内存中开辟一块独立的栈空间,来执行该线程的任务代码。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程i的值是:" + i);
}
}
}
public class Test {
public static void main(String[] args) {
// 创建并启动子线程
MyThread mt = new MyThread();
mt.start();
// 主线程
for (int j = 0; j < 100; j++) {
System.out.println("主线程j的值是:" + j);
}
}
}
3、JMM 内存模型(Java Memory Model, JMM)
3.1、Java 虚拟机规范中定义的一种内存模型
该模型描述了java程序中共享变量的访问规则,以及在JVM中变量存储到内存与从内存中读取到变量的底层细节。
简而言之:
所有的共享变量都存储在主内存中,每一个线程都有自己的工作内存,工作内存是线程隔离的。线程对变量的操作都需要将变量拷贝一份到自己的工作内存中,操作完修改后,再重新写回到主内存。
4、多线程的安全问题:
4.1、可见性
4.1.1、出现可见性问题的原因:
一条线程对共享变量的修改,其他线程不可见
4.2、有序性
4.1.2、出现有序性问题的原因:
1、 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1
int b = 20; //2
int c = a + b; //3
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。
2、但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
4.3、原子性
4.3.1、什么是原子性:
是指在一次操作或多次操作中,要么所有的操作都得到了执行,并且不会受其他因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可分割的整体。
4.3.2、出现原子性问题的原因:
多条线程对共享变量操作产生覆盖的效果。
5、解决多线程中的安全性问题
5.1、加锁(Synchronized),使用同步机制
1、加锁可以解决线程中所有的安全性问题
2、它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。
3、synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
5.1.1、同步代码块:
概述:使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问。
锁对象:
- 语法的角度: 锁对象可以是任意类的对象
- 同步的角度: 多条线程想要实现同步,那么这多条线程使用的锁对象要一致(相同)
案例:解决买票的线程安全问题
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 加锁
synchronized (this){
// 条件判断
if (tickets < 1) {
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
}
// 释放锁
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
}
}
5.1.2、同步方法:
概述: 使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问。
锁对象:
- 非静态同步方法锁对象是: this
- 静态同步方法锁对象是: 该方法所在类的字节码对象—>类名.class
案例:
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 条件判断
if (sellTickets()) break;
}
}
// 非静态同步方法
private synchronized boolean sellTickets() {
if (tickets < 1){
return true;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
return false;
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
}
}
5.1.3、Lock锁
概述: 也是一种锁,他比synchronized更加强大,更加面向对象
使用:
1、Lock是一个接口,Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作
2、使用Lock就需要使用Lock接口的实现类ReentrantLock
案例:解决买票案例:
public class MyRunnable implements Runnable {
// 共享变量--被4条线程共享
int tickets = 100;
// 创建Lock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
// 线程的任务代码----->卖票
// 循环卖票,直到没有票为止
while (true) {
// 条件判断
// 加锁
lock.lock();
if (tickets < 1){
// 释放锁
lock.unlock();
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":线程正在出售第" + tickets + "张票");
tickets--;
// 释放锁
lock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
// 创建任务对象
MyRunnable mr = new MyRunnable();
// 创建并启动4条线程
new Thread(mr,"窗口1").start();
new Thread(mr,"窗口2").start();
new Thread(mr,"窗口3").start();
new Thread(mr,"窗口4").start();
/*
注意:
1.线程锁对象没有释放,线程就不会销毁
2.子线程没有销毁,主线程就不能结束\销毁
*/
}
}
5.2、Volatile关键字
概述: 它是一个修饰符,只能用来修饰成员变量
volatile可以解决可见性,有序性问题,但是不能解决原子性问题
作用:
1.被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值
2.被volatile修饰的成员变量,可以保证不会被编译器重排
5.3、原子(Atomic)类
概述: java.util.concurrent.atomic包中提供了很多原子类,这些原子类在多线程的环境下是线程安全的
作用: 可以解决原子性问题,可见性问题,有序性问题
原子类的工作原理(CAS机制):
CAS机制: 比较并交换, 拿刚刚从主内存中获取的值 与 当前主内存中的值进行比较,如果相同,就把自增1后的值跟主内存中的值进行交换,如果不相同,就不交换,而是从新获取主内存中的值,再进行比较并交换,…
AtomicInteger类:
public AtomicInteger();创建一个AtomicInteger对象,表示整数0
public AtomicInteger(int nul);创建一个AtomicInteger对象,表示指定整数
public final int getAndIncrement(); 自增1
public final int get(); 获取当前对象表示的整数值
案例:
public class MyThread extends Thread {
// 共享变量
// volatile static int a = 0;
static AtomicInteger a = new AtomicInteger(0);
@Override
public void run() {
// 子线程对a自增10万次
for (int i = 0; i < 100000; i++) {
a.getAndIncrement();
}
System.out.println("子线程操作完毕!");
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
// 创建并启动线程
MyThread mt = new MyThread();
mt.start();
// 主线程对a自增10万次
for (int i = 0; i < 100000; i++) {
MyThread.a.getAndIncrement();
}
// 为了保证主线程和子线程都执行完毕,再打印最终a的值
Thread.sleep(3000);
System.out.println("最终a的值:"+ MyThread.a.get());
}
}
三、并发包
在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。
1、CopyOnWriteArrayList线程安全
案例:
public class MyThread extends Thread {
// 共享变量---ArrayList
//static ArrayList<Integer> list = new ArrayList<>();
static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
@Override
public void run() {
// 线程任务:往集合中添加10000个元素
for (int i = 0; i < 10000; i++) {
list.add(i);
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 案例: 2条线程同时往集合中分别添加10000个元素
// 创建并启动2条线程
new MyThread().start();
new MyThread().start();
// 为了保证2条集合都操作完毕,再来打印集合元素个数
Thread.sleep(2000);
System.out.println("集合元素个数:" + MyThread.list.size());
/*
结果: list集合的元素个数一定2万个
*/
}
}
2、CopyOnWriteArraySet线程安全
public class MyThread extends Thread {
// 共享变量---HashSet
//static HashSet<Integer> set = new HashSet<>();
static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
@Override
public void run() {
// 线程任务:往集合中添加10000个元素
for (int i = 0; i < 10000; i++) {
set.add(i);
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 案例: 2条线程同时往集合中分别添加10000个元素
// 创建并启动2条线程
new MyThread().start();
new MyThread().start();
// 为了保证2条集合都操作完毕,再来打印集合元素个数
Thread.sleep(2000);
System.out.println("集合元素个数:" + MyThread.set.size());
/*
结果: set集合的元素个数一定是1万个
*/
}
}
3、ConcurrentHashMap线程安全
public class MyThread extends Thread {
// 共享变量---HashSet
//static HashMap<Integer,Integer> map = new HashMap<>();
//static Hashtable<Integer,Integer> map = new Hashtable<>();
static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
@Override
public void run() {
// 线程任务:往集合中添加10000个元素
for (int i = 0; i < 10000; i++) {
map.put(i,i);
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 案例: 2条线程同时往HashMap集合中添加10000个键值对
// 创建并启动2条线程
new MyThread().start();
new MyThread().start();
// 为了保证2条集合都操作完毕,再来打印集合元素个数
Thread.sleep(2000);
System.out.println("集合元素个数:" + MyThread.map.size());
/*
结果: Map集合的元素个数一定是1万个
*/
}
}
3.1、ConcurrentHashMap和HashTable的效率问题:
3.1.1、HashTable效率低下原因:
public synchronized V put(K key, V value)
public synchronized V get(Object key)
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3.1.2、ConcurrentHashMap高效的原因:
CAS + 局部(synchronized)锁定
4、CountDownLatch
概述:CountDownLatch允许一个或多个线程等待其他线程完成操作。
public class MyThread1 extends Thread {
CountDownLatch cdl;
public MyThread1(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
System.out.println("线程1: 打印A....");
// 打印完A后,进入等待
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1: 打印C....");
}
}
public class MyThread2 extends Thread {
CountDownLatch cdl;
public MyThread2(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
System.out.println("线程2: 打印B....");
// 打印完B后,计数器-1
cdl.countDown();
}
}
public class Test {
public static void main(String[] args)throws Exception{
CountDownLatch cdl = new CountDownLatch(1);
new MyThread1(cdl).start();
Thread.sleep(100);
new MyThread2(cdl).start();
}
}
// 注意: 线程1和线程2需要使用同一个CountDownLatch对象
5、CyclicBarrier
作用: 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
案例演示:
- 例如: 公司召集5名员工开会,等5名员工都到了,会议开始
public class MyRunnable implements Runnable {
CyclicBarrier cb;
public MyRunnable(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":到达会议室...");
// 进入线程等待
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":离开会议室...");
}
}
public class Test {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("开始开会....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("会议结束,大家可以自行离开...");
}
});
// 创建任务对象
MyRunnable mr = new MyRunnable(cb);
// 创建5条线程并启动
new Thread(mr,"员工1").start();
new Thread(mr,"员工2").start();
new Thread(mr,"员工3").start();
new Thread(mr,"员工4").start();
new Thread(mr,"员工5").start();
}
}
6、Semaphore
作用: Semaphore的主要作用是控制线程的并发数量。
常用方法:
public Semaphore(int permits) permits 表示许可线程的数量
public void acquire() 表示获取许可
public void release() 表示释放许可
案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
public class HuiSuo {
Semaphore sp;
public HuiSuo(Semaphore sp) {
this.sp = sp;
}
public void comeInRoom(){
// 获得许可证---获取手牌
try {
sp.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 按摩洗脚
System.out.println(Thread.currentThread().getName()+":拿到手牌,正在享受18号技师按摩洗脚服务...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":归还手牌,结束18号技师按摩洗脚服务...");
// 释放许可证---归还手牌
sp.release();
}
}
public class Test {
public static void main(String[] args) {
// 案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
Semaphore sp = new Semaphore(2);
// 创建会所对象
HuiSuo hs = new HuiSuo(sp);
// 创建5条线程并启动
new Thread(new Runnable() {
@Override
public void run() {
// 进入浴室
hs.comeInRoom();
}
},"1号顾客").start();
new Thread(new Runnable() {
@Override
public void run() {
// 进入浴室
hs.comeInRoom();
}
},"2号顾客").start();
new Thread(new Runnable() {
@Override
public void run() {
// 进入浴室
hs.comeInRoom();
}
},"3号顾客").start();
new Thread(new Runnable() {
@Override
public void run() {
// 进入浴室
hs.comeInRoom();
}
},"4号顾客").start();
new Thread(new Runnable() {
@Override
public void run() {
// 进入浴室
hs.comeInRoom();
}
},"5号顾客").start();
}
}
7、Exchanger
作用:是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
常用方法:
public Exchanger()
public V exchange(V x) 参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据
public class MyThread1 extends Thread {
Exchanger<String> ex;
public MyThread1(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
// 任务: 把itheima字符串传给B线程
System.out.println("A线程:准备把itheima传递给B线程...");
try {
String msgB = ex.exchange("itheima");
System.out.println("A线程: B线程传递过来的数据"+msgB);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyThread2 extends Thread {
Exchanger<String> ex;
public MyThread2(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
// 任务: itcast字符串传给A线程
System.out.println("B线程:准备把itcast传递给A线程...");
try {
String msgA = ex.exchange("itcast");
System.out.println("B线程: A线程传递过来的数据"+msgA);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
// 案例演示: AB两条线程交换字符串数据
// eg: A线程: itheima字符串传给B线程
// eg: B线程: itcast字符串传给A线程
Exchanger<String> ex = new Exchanger<>();
// 创建并启动2条线程
new MyThread1(ex).start();
new MyThread2(ex).start();
}
}
四、线程的状态
线程从创建到销毁的过程称为线程的生命周期,在线程的生命周期内一共有六种状态:
java后续基础内容正在快马加鞭更新中,内容若有误,欢迎各位码友提出建设性意见。