目录
一、引言
本文将介绍Java中多线程的一些相关知识,用于尽快熟悉回忆。因此,这篇文章并不是一个专业性介绍多线程的文章,为了学习和爱好,书写文章笔记。如有错误,欢迎批评指正,感激不尽!
二、进程和线程
1、进程
进程简单的说就是一个正在执行的程序,比如:QQ,IDEA。进程是系统分配资源的最小单位。windows系统可以通过任务管理器查看当前运行的进程。
2、线程
线程是由进程分配创建的,是进程的一个实体,具体干活的人,一个进程可以有多个线程。线程不独立分配内存,而是和进程共享内存资源,线程可以共享CPU的计算资源,所以说线程是CPU资源调度的最小单位。
三、Java中线程的创建方式
1.继承Thread类,重写run方法;
public class ThreadImpl1{
public static void main(String[] args) {
System.out.println("当前线程:"+Thread.currentThread().getName());
MyThread mt = new MyThread();
mt.start();
}
static class MyThread extends Thread{
@Override
public void run(){
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
}
2.实现Runnable接口,重写run方法
public class ThreadImpl2 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
});
t1.start();
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
3.实现Callable接口--带有返回值的线程
public class ThreadImpl3 {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getName());
return 111;
}
});
Thread t1 = new Thread(task);
t1.start();
//该方法会阻塞
Integer integer = task.get();
System.out.println("线程执行返回的值:"+integer);
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}
四、线程的生命周期
线程的生命周期中,总共有六中状态
状态 | 描述 |
---|---|
【NEW】 | 新建状态:java中使用new关键字创建线程,分配内存和初始化,未执行start()方法之前 |
【RUNNABLE】 | 就绪状态:线程正在JVM中被执行,等待来自操作系统(如处理器)的调度。 |
【BLOCKED】 | 阻塞,因为某些原因不能立即执行需要挂起等待。 |
【WAITING】 | 无限期等待,由于线程调用了Object.wait(0) ,Thread.join(0) 和LockSupport.park 其中的一个方法,线程处于等待状态,其中调用wait , join 方法时未设置超时时间。 |
【TIMED_WAITING】 | 有限期等待, 线程等待一个指定的时间,比如线程调用了Object.wait(long) , Thread.join(long) ,LockSupport.parkNanos , LockSupport.parkUntil 方法之后,线程的状态就会变成TIMED_WAITING |
【TERMINATED】 | 终止的线程状态,线程已经完成执行。 |
线程中操作的方法还有许多,需要可参考相关资料查阅。
五、JMM-java内存模型
Java虚拟规范中曾试图定义一种Java内存模型,来屏蔽各种硬件和操作系统的内存访问之间的差异,以实现让java程序在各种平台上都能达到一致的内存访问效果。所以JMM本身是一个抽象的概念,并不真是存在,它是一种规则或规范。
我们知道,计算机在执行程序时,指令都在CPU中进行的,执行指令的过程中会涉及到数据的读取和写入,而程序运行时的数据是存放在主存中的,由于CPU的执行速度很快,而从内存读取数据和写入的过程相对来说较慢,那么就会大大影响CPU的执行速度(寄存器 > L1 cache > L2 cache > L3 cache> 主存),因此在CPU中就有了高速缓存。当程序运行时,CPU会从主存中拉去数据到缓存,运算结束后再刷回主存。
而主流程序语言直接使用物理内存和操作系统的内存模型,会由于不同平台的内存模型的差异,可能导致程序在一套平台上发挥完全正常,而在另一套平台上经常发生并发错误,所以在某种常见的场景下,必须针对平台来进行代码的编写。
1、指令重排
在指令重排中,有一个经典的 as-if-serial语义,计算机会对指令进行优化,其不管怎么重新排序,程序的执行结果不能改变,为了遵守as-if-serial语义,编译期和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果,但是,如果操作之间不存在数据依赖关系。可能会被编译期和处理器重指令。
大概意思是:
int a = 1;
int b = 2;
int c = a + b;
如上代码:正常来说,应该是顺序执行,但由于指令重排,也可能是 int b =2 先执行。然后 int a = 1;在执行。 最后 int c = a+b; 那为什么最后一句不可能被乱序呢,这是由于int c = a+b;
存在依赖关系,a和b都没有被赋值,没有值进行运算会报错的。可以说在单线程情况下,指令重排能够提升效率。而在多线程下,由于线程和线程之间是无感知的,当一个线程依赖另一个线程的数据,指令重排就可能会发生问题。
那么如何解决指令重排的问题? Java中使用一个volatile关键字来保证一个变量在一次读写操作时避免指令重排,大概原理是内存屏障,在读写操作之前一条指令,当cpu碰到这条指令必须等待前面的执行完成才能执行下一条执行。
2、可见性
还有一个可见性问题。我们知道cpu中为了解决cpu执行快而从主存中读取慢的问题,加入了高速缓存。而一个Thread线程一直在高速读取缓存的数据,不能感知到主线程已经在主存修改了改数据,这也就造成了可见性的问题。 Java中同样使用Volatile关键字解决,它能强制对改变的量的读写直接在主存中操作,从而解决不可见的问题。写操作就是立刻刷到主存,将其他缓冲区域的值设置为不可用。
六、锁机制
为了解决多线程环境下,由于线程间进行竞争的资源等,导致的数据一致性等问题,引入锁机制。我们通常在使用对象或调用方法之前进行加锁,这时如果有其他线程也需要使用该对象或者调用该方法,则先需要获取锁,如果发现锁被其他线程占用,就会进入阻塞队列等待锁的释放,直到其他线程执行完毕释放锁,该线程才有再次获取锁并执行操作。
从悲观和乐观的角度分:可以分为悲观锁和乐观锁。
从获取资源的公平性角度分:可以分为公平锁和非公平锁
从是否共享的角度分:可以分为共享锁和独占锁。
从状态的角度分:可以分为偏向锁、轻量级锁和重量级锁
1、悲观锁和乐观锁
悲观锁采用悲观的思想:在每次读取数据时,都会认为别人会修改数据,所以在读写数据时都会加锁,这样在别人想读写这个数据时就会阻塞、等待直到获取锁。
Java中的悲观锁大部分基于AQS( Abstract Queued Synchronized ,抽象的队列同步器)架构实现。AQS简单来说就是通过维护一个共享资源状态(State)和一个先进先出(FIFO)的队列来实现一个多线程访问共享资源的同步框架。例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。AQS框架会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁。
乐观锁采用乐观的思想:在每次读取数据时都会认为别人不会修改改数据,所以就不会加锁,但在更新时会判断再此期间别人有没有更新数据,通常采用在写的时候先读出当前版本号然后加锁的方法。具体过程:比如说当前版本号version = 1 ,A和B这两个线程同时操作,先读取数据(包括版本号),如果A比B操作快,就先一步刷回数据,然后把version改为2,当B操作完对比version的时候不相等,则再次重复。
Java中的乐观锁大部分 是通过CAS( Compare And Swap,比较并交换) 操作实现,CAS是一种原子性更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样。如果一样则更新,否则不执行更新,直接返回操作失败。
2、synchronized
synchronized属于独占式的悲观锁,同时属于可重入锁,很多人称呼它为重量级锁,随着Java se 1.6对synchronized进行了优化之后,有些情况下它并不是那么重。
它有三种方式加锁:
1、修饰实例方法,作用于当前实例加锁
2、静态方法,作用于当前类对象加锁
3、修饰代码块,制定加锁对象,给定对象加锁。
synchronized原理:
通过Java的反编译器Javac 观察字节码文件,发现加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
synchronized的原理还挺复杂的,我对此理解甚少,jdk1.6之后对synchronized进行了改进,引入了一个锁升级的概念。锁升级的过程涉及四把锁:无锁、偏向锁、轻量级锁、重量级锁。
jvm通过当前业务的线程操作情况,一步步的完成锁升级。
3、死锁
死锁就是多个线程同时被阻塞,它们都在等待某个占用资源的线程释放锁。导致程序不可能正常终止。
产生死锁的四个必要条件:
1、互斥使用,即当资源被一个线程占用时,别的线程不能使用。
2、不可抢占,资源请求者不能强制从资源占用者手中夺取资源,资源只能由资源占用者主动释放。
3、请求和保持,即当资源请求者在请求其他资源的同时保持对原资源的占有
4、循环等待,即存在一个等待序列:a占有b的资源,b占有c的资源,c占有a的资源,这样形成了一个环路。
只要打破上述任意一个条件便可破除死锁。
4、可重入锁
可重入锁也叫递归锁,指在同一线程中外层函数获取到该锁之后,内层的递归函数任然可以继续获取锁。ReentrantLock和synchronized都是可重入锁。
5、公平锁和非公平锁
公平锁:是指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
非公平锁:指在锁分配时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时在排到队尾等待。
6、读写锁
在Java中通过Lock接口及对象可以方便地为对象加锁。但是这种锁不区分读写,叫做普通锁。为了提高性能,Java提供了读写锁。读写锁分为读锁和写锁。多个读锁不互斥,读锁和写锁互斥。
Java中读写锁的用法:
private final Lock readLock = rwlock.readLock(); //定义读锁
private final Lock writeLock= rwlock.writeLock(); //定义写锁
public String read(){
readLock.lock();
try{
return "读到我了";
}
finally{
readLock.unlock();
}
}
public String write(){
writeLock.lock();
try{
return "我在写数据";
}
finally{
writeLock.unlock();
}
}
7.共享锁和独占锁
独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现
共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantLockReadWriteLock中的读锁位共享锁的实现。
8、重量级锁和轻量级锁、偏向锁
重量级锁是基于操作系统的互斥量实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。synchronized在内部基于监视器锁(Monitor)实现,监视器锁是基于底层代码实现。因此synchronized属于重量级锁。重量级锁需要再用户态和内核态之间来回切换,所有synchronized的运行效率不高。
前面说过,jdk1.6版本之后,为了减少获取锁和释放锁带来的性能消耗以及提高性能,引入了偏向锁和轻量级锁等。
偏向锁,当只有一个线程或者经常存在同一个锁被同一个线程多次获取的情况。偏向锁能消除线程锁重入的开销。偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS原子操作。而偏向锁只需切换ThreadID时执行一次CAS。
9、如何进行锁优化
1.减少对锁的持有时间
减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。
2、减小锁粒度
减小锁粒度指将单个消耗时较多的锁操作拆分为多个耗时较少的锁操作来减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量锁的使用率才会高。
3、锁分离
指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁,它根据锁的功能将锁分离为了读锁和写锁。这样读不互斥,读写互斥,写写互斥,既保证了线程的安全,又提高了性能。
4、锁粗化
指为了保障性能,要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细了,则将会导致系统频繁获取锁和释放锁,反而影响性能的提升。因此将关联性强的锁操作集中起来处理。
5、锁消除
锁消除是由于不规范的使用锁操作,比如在不需要锁的情况下误用锁操作导致性能下降。因此在编码时需要细心思考和检查。
七、总结
多线程的概念,以上没有解释并发和并行,这里补充一下,并发就是一个同时不间断的干多件事,并且不停的来回切换。并行就是多个人各负责各自的那份工作。
Java中线程的创建方式,线程的生命周期,JMM的简单介绍,指令重排,可见性,锁机制。
个人比较懒,为了养成总结的习惯,嘿嘿,强迫自己写写,免得容易忘记
Java中的多线程知识有很多是底层的代码实现,想更深入的了解必须得深入JVM底层,但其中很多有趣厉害的设计思想也有在java中实现。