引言
一家公司账户上有1000块钱,这天两个会计同时操作了公司的银行账户,会计A存钱100,会计B取钱50,可能会发生如下几种情况:
1 账户余额变成1100;(A和B同时读到了账户余额等于1000,A最后更改了账户金额)
2 账户余额变成950;(A和B同时读到了账户余额等于1000,B最后更改了商户金额)
3 账户余额变成1050;(A和B不是同时读到账户余额)
1 线程与进程
进程:
- 执行中的应用程序
- 一个进程可以包含一个或多个线程
- 一个进程至少要包含一个线程(比如java中运行main方法的主线程)
比如:我们在哔哩哔哩看视频的同时,还可以点赞、收藏、刷弹幕等等,这些操作都是哔哩哔哩这个视频应用程序的子任务,也就是这个进程的子线程;
线程:
- 线程本身依靠进程(程序)运行
- 线程是程序中的顺序控制流,只能使用分配给程序的资源和环境
解释:
运行一个Java程序的实质是启动一个Java虚拟机进程,也就是说一个运行的Java程序就是一个Java虚拟机进程。
线程是进程中可独立执行的最小的执行单元,一个进程中有多个线程,同一进程中的线程共享该进程的资源(内存,空间,变量,方法)。
单线程:
程序中只存在一个主线程,实际上主方法(main)就是一个主线程
线程所要完成的计算过程被称为任务
多线程:多线程是在一个程序中同时运行多个任务
多线程的目的是:更好的使用CPU资源,提高程序的计算效率
2 串行、并发与并行
串行:一个人,顺序的做多个任务;总耗时是多个任务耗时总和
并发:一个人,交替的做多个任务;
即一个人先执行一个任务,在这个任务执行期间,有一段时间人是空闲的,他利用这段时间再去执行另一个任务,这样交替执行任务。
示例:煮馒头的同时,在馒头熟之前,这段时间里我们还可以拖地、洗衣服等等。
并行:多个人,同时做多个任务;总耗时取决于用时最长的任务
3 创建线程的方式
在java中,线程的实现有3种方式
3.1 extends继承Thread类,重写run()方法
public class Worker extends Thread{
@Override
public void run() {
}
}
3.2 implements实现Runnable接口,重写run()方法
public class Worker2 implements Runnable{
@Override
public void run() {
}
}
3.3 通过Callable接口和FutureTask对象创建线程
- 自定义一个类实现Callable接口并实现其中的call方法来创建线程任务(Runnable对Callable,run()方法对call()方法);
- 接着把这个线程任务以参数的形式传递给FutureTask,并new出一个FutureTask对象;
- 最后将这个FutureTask对象继续以参数的形式传递给Thread
启动线程:
//创建一个Thread对象,加载线程类对象,并且给线程命名
Worker2 producter = new Worker2();
Thread t1 = new Thread(producter, "thread1");
t1.start();//调用start()方法不是立即去调用run(),而是先去争用CPU时间片,一旦获取CPU时间片,才回去调用run()方法
/*
* start启动原理
* 1 调用Thread对象的run方法
* 2 thread.run()方法,调用Runable target对象的run方法;
*/
4 线程生命周期(重点)
- 新建状态(New):线程对象创建后,即进入新建状态;
- 就绪状态(Runnable):当调用线程对象的start()方法,线程进入就绪状态。处于就绪状态的线程,只是说明该线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start()该线程就会立即执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才开始执行,即进入运行状态。注意,就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态。直到其重新进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为3种:
1. 等待阻塞:运行状态中的线程执行Object.wait()方法
2. 同步阻塞:线程在获取synchronized内部锁失败(因为锁被其它线程所占用)
3. 其它阻塞:通过调用线程的Thread.sleep()、join()方法或发出I/O请求时,线程会进入到阻塞状态。当sleep()状态超时(sleep休眠结束)、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态
- 死亡状态(Dead):线程执行完毕或者因异常退出了run()方法,该线程结束生命周期,线程销毁。
线程声明周期示意图
join()加入
将出于阻塞状态的线程加入到CPU的调度队列中,让该线程等待CPU调度(分配计算资源),此时该线程出于就绪状态;
yield()礼让
处于运行状态的线程,将CPU资源让出,交给其他线程使用;礼让的线程重新处于就绪状态等待CPU调度;
interrupt()
中断线程,实际上只是给线程设置一个中断标志,线程仍会继续运行;
5 线程的特性
5.1 安全性
一般而言,如果一个类在单线程环境下能够运作正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全的(thread safe)
5.2 原子性Atomicity
原子性的字面意思是不可分割的,对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该线程就是原子操作,也称该操作具有原子性。
所谓的不可分割,其中一个含义是指访问(读、写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会“看到”该操作已经执行了部分的中间效果。
实现原子性的方式有两种:
- 使用锁(Lock/synchronized),锁具有排他性,即他能够保障一个共享变量在任意一个时刻只能够被一个线程访问。
- 利用处理器提供的专门CAS(Compare-and-Swap)指令,CAS指令实现原子性的方式与锁实现原子性的方式实质上是相同的,差别在于锁通常是在软件这一层次实现的,而CAS是直接在硬件(处理器和内存)这一层次实现的,他可以看做“硬件锁”。
在Java语言中,long和double类型以外的任何类型的变量的写操作都是原子性的。
用volatile关键字修饰的long和double类型的写操作是原子性的。
private volatile long value;
5.3 可见性Visibility
如果一个线程A对共享变量B进行更新之后,后续其他访问变量B的线程可以读取到该更新的结果,那么我们就称线程A对共享变量B的更新对其他线程可见。否则我们就称这个线程A对共享变量B的更新对其他线程不可见。
变量值发生变化(写操作)----缓冲区---高速缓存---内存
为了保障可见性,必须使一个处理器对共享变量所做的更新最终被写入到该处理器的高速缓存或主内存中(而不是始终停留在其写缓冲器中),这个过程被称为冲刷处理器缓存(写线程)。
一个处理器在读取共享变量的时候,如果其他处理器在此之前已经更新了该变量,那么该处理器必须从其他处理器的高速缓存或者主内存中对相应的变量进行缓存同步,这个过程被称为刷新处理器缓存(读线程)。
通过使用volatile关键字修饰变量,来保障变量的可见性。
volatile关键字所起到的一个作用就是,提示JIT编译器被修饰的变量可能被多个线程共享,以阻止JIT编译器做出可能导致程序运行不正常的优化(阻止指令重排序)。
另一个作用就是读取一个volatile关键字修饰的变量,会使相应的处理器执行刷新处理器缓存的动作;写一个volatile关键字修饰的变量,会使相应的处理器执行冲刷处理器缓存的动作,从而保障了可见性。
5.4 有序性Ordering
指令重排序,是编译器出于性能的考虑,在其认为不影响程序(单线程环境)正确性的情况下可能会对源代码顺序进行调整,从而造成程序顺序与源代码顺序不一致的情况。
javac:静态编译器,将源代码java文件编译成字节码class二进制文件;
JIT:动态编译器,将字节码动态编译为JVM宿主机的本地机器码;
volatile关键字具有防止指令重排序的作用,通过在volatile声明的变量前后插入内存屏障,保证了被volatile声明的变量不会发生重排序。
6 线程同步机制
线程同步机制是一套用于协调线程间的数据访问及活动的机制,该机制用于保障线程安全以及实现这些线程的最终目标。
广义上说,Java平台提供的线程同步机制包括:锁(Lock/synchronized)、volatile关键字、final关键字、static关键字以及一些相关的API,如Object.wait()/Object.notify()等。
7 内部锁:synchronized关键字
Java中的任何一个对象,都有唯一一个与之关联的锁。这种锁被称为监听器(Monitor)或内部锁(Intrinsic Lock)。内部锁是一种排它锁,他能够保障原子性、可见性和有序性。
内部锁是通过synchronized关键字实现的。synchronized关键字可以用来修饰方法以及代码块。synchronized关键字修饰的方法称为同步方法(synchronized method),synchronized关键字修饰的静态方法称为同步静态方法,synchronized修饰的代码块称为同步代码块(synchronized block)。
核心:多个线程争用一把锁,谁拿到锁,才能调用锁关键字声明的方法
示例:
public class Account implements Runnable{
private static int money = 0;
/*
synchronized 内部锁:是一种排他锁,它能够保障原子性、可见性和有序性
每次只有一个线程能拿到锁,即每次只能有一个线程调用同步锁声明的资源
*/
private synchronized static void doMoney() {
for(int i = 0;i < 50;i++) {
try {
/*
sleep()方法属于Thread类,作用是当前线程休眠指定的时间,但是不释放线程资源;当休眠时间结束后,继续执行后面的代码。
*/
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "," + ++money);
}
}
@Override
public void run() {
doMoney();
}
}
public class AccountRun {
//执行类
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new Thread(account,"t1");
Thread t2 = new Thread(account,"t2");
//启动线程
t1.start();
t2.start();
}
}
8 wait()和sleep(long time)的区别(重要)
sleep() 休眠
属于Thread类,是static修饰的静态方法;
当前线程休眠time 毫秒,线程处于阻塞状态,让出CPU资源,但是不让出锁,休眠结束进入就绪状态等待继续执行sleep()方法后面的代码;
wait() 等待
属于Object类;
线程获取同步锁后,执行wait()方法会主动释放锁,同时本线程进入阻塞状态,让出CPU资源,让出锁。直到其他线程执行notify()或者notifyAll()方法唤醒该线程后,进入就绪状态,才能继续获取同步锁,并继续执行。
所属类 | 作用 | 让出CPU资源 | 让出锁 | 唤醒 | |
wait() | Object | 线程等待 | 让出 | 让出 | 其他线程调用notify()或notifyAll()方法唤醒 |
sleep() | Thread | 线程休眠 | 让出 | 不让出 | 休眠结束自动唤醒 |
notify():
属于Object类;
唤醒正在等待此对象的监听器(处于阻塞状态的线程),如果有任何线程正在等待此对象,请选择其中一个线程唤醒。这种选择是任意的,取决于线程的优先级。
notifyAll():
属于Object类;
唤醒所有处于阻塞状态的线程;
9 显式锁:Lock接口
Lock接口有两个实现类:ReentrantLock和ReadWriteLock;
ReentrantLock
既可以实现公平锁,也可以实现非公平锁;
Lock lock = new ReentrantLock(true);//true-公平锁 false-非公平锁,默认false
锁的排他性使得多个线程无法以线程安全的方式在同一时刻对共享变量进行读操作,这不利于提高系统的并发性。由此产生了ReadWriteLock。
ReadWriteLock
读写锁允许多个线程可以同时只读共享变量,但是一次只允许一个线程对共享变量进行更新操作。
final ReadWriteLock rwLock = new ReentrantReadWriteLock();
final Lock readLock = rwLock.readLock();//创建读锁对象
final Lock writeLock = rwLock.writeLock();//创建写锁对象
readLock.lock();//获取读锁
readLock.unlock();//释放读锁
10 生产消费模式
工厂类
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Depot {
//仓库最大容量
private final int MAX_SIZE = 5;
//仓库存贮载体
private LinkedList<Object> list = new LinkedList<Object>();
//true-表示线程调度策略为-公平锁,默认false
ReentrantLock lock = new ReentrantLock(true);
//生产者锁管理对象
Condition condition_product = lock.newCondition();
//消费者锁管理对象
Condition condition_consume = lock.newCondition();
//生产
public void product(){
lock.lock();//加锁
//获取线程名称
String name = Thread.currentThread().getName();
while (list.size() == MAX_SIZE){
try {
System.out.println(name + "--生产者--库存已满,暂停生产");
//暂停线程并让出锁,等价于wait()方法
condition_product.await();
/*
* Condition的await()方法,个人理解有两个功能:
* 1. 暂停当前线程并让出锁
* 2. 将当前线程分类到当前Condition类对象中
*/
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(name + "线程恢复,继续生产");
}
}
list.add(new Object());
System.out.println(name + "--生产者--生产完成,当前库存"+list.size());
condition_consume.signal();//唤起消费者线程
lock.unlock();//释放锁
}
//消费
public void consume(){
lock.lock();
//获取线程名称
String name = Thread.currentThread().getName();
while (list.size() == 0) {
try {
System.out.println(name +"--消费者--库存已空,暂停消费");
condition_consume.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println(name + "--消费者--消费完成,当前库存"+list.size());
condition_product.signal();//唤起生产者线程
lock.unlock();
}
}
生产者类
public class Producter implements Runnable{
private Depot depot;
public Producter(Depot depot){
this.depot = depot;
}
public void run() {
while (true) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
depot.product();
}
}
}
消费者类
public class Consumer implements Runnable{
private Depot depot;
public Consumer(Depot depot){
this.depot = depot;
}
public void run() {
while (true) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
depot.consume(name);
}
}
}
运行类
public class Main {
public static void main(String[] args) {
//创建工厂类对象,注意锁的概念(多个线程争用同一把锁),所以下面创建的线程要共用同一个工厂类对象
Depot depot = new Depot();
//创建4个生产者线程
for (int i = 0; i < 4; i++) {
new Thread(new Producter(depot),"p"+i).start();
}
//创建3个消费者线程
for (int i = 0; i < 3; i++) {
new Thread(new Consumer(depot),"c"+i).start();
}
}
}
//注:本人可能由于学习不精,导致文章中出现错误,如有发现,欢迎指导批评,感激不尽。