多线程与高并发
多线程相关
主要了解线程相关一些知识,例如线程组成,synchronize字段,锁等等
线程
java中包含进程,线程,纤程三种概念,一个JVM实例其实就是JVM跑起来的进程,二者合起来称之为一个JAVA进程,一个进程可以有多个线程。
什么是线程
- 操作系统:线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- jvm:线程与进程相似但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
java中线程启动的方式
- 通过继承Thread类,重写run方法,并调用start方法,实际上Thread类也实现Runnable接口
* @author unascribed
* @see Runnable
* @see Runtime#exit(int)
* @see #run()
* @see #stop()
* @since JDK1.0
*/
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
- 通过实现Runnable类,实现run方法,并通过new Thread(Runnable target).start()方法来启动
- 通过线程池来启动线程
线程的几种状态
- New(新建):创建一个线程对象,此时未调用start()方法,没有运行线程中的代码。
- Runnable(就绪):调用了线程对象的start()方法,在start()方法返回后,线程处于就绪状态,该线程位于可运行线程池中,等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
- Running(运行):就绪态的线程获取了CPU,执行了run()方法。
- Blocked(阻塞):线程因为某种原因放弃CPU的使用权,暂时停止运行。
- Dead(死亡):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
阻塞分为三种:
等待阻塞:该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。这个状态需要等待其他线程唤醒,例如join()与wait()方法。
同步阻塞:运行中的线程在获取对象的同步锁时,若该锁现被其他线程占用,则JVM会将线程放入“锁池”。
其他阻塞:运行中的线程执行了sleep(),或者发出I/O请求,线程等待处理完毕后重新转入就绪状态,这种阻塞不会释放锁占用的资源。
interrupt
- interrupt()方法:其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
- interrupted()方法:作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。
- isInterrupted()方法:作用是只测试此线程是否被中断 ,不清除中断状态。
一般在程序中不会主动使用interrupt方式,除非遇到一些比较长时间等待又有必要中断该线程的阻塞强行执行,通过捕获interruptException继续执行。
关键字
synchronize
synchronize使用的几种方式
- 修饰普通方法:锁对象就是当前对象也就是this
public class ThreadTest{
public synchronize void test(){
//锁住的是对象本身 等同于 synchronize(this)
System.out.println("test");
}
}
- 修饰静态方法:锁对象是当前Class对象
public class ThreadTest{
public static synchronize void test(){
//锁住的是类对象,也就是synchronize(ThreadTest.class)
System.out.println("test");
}
}
- 修饰代码块:通过synchronize对象中的成员变量锁住该代码块
public class ThreadTest{
private Object o = new Object();
public void test(){
synchronize(o){
//锁住的是o对象
System.out.println("test");
}
}
}
锁代码块只是一个概念,实际上所有的锁锁住的都是对象,每个对象在地址的前两位就是一个锁的标识位,代表这个对象是否被锁住。
Class本身也是一个对象,因此锁住的也同样是对象,只不过这个对象相对特殊。是类对象。
synchronize一些注意点
- synchronize保证了原子性和可见性,因此在使用了synchronize时,可以不再使用volatile去保证锁住的对象的可见性。
- synchronize是可重入锁,如果synchronize不可重入,那么当父类的一个方法synchronize时,子类方法重写时调用super的同方法会造成死锁。
- synchronize在异常时,会释放其锁住的对象。
- synchronize锁住对象时,不要锁住String,Integer,Long等基础类型包装类,因为例如String(非new String())是常量,Integer -128~127也同样是常量。就算本身程序中能保证只有一块在锁。但是jar包中也同样有可能会有相同的常量会被锁住,造成不可预见的问题。
- synchronize是一个锁升级的过程,所以性能同样很高,不必比ock差
- synchronize锁可升级,但是不可降级
- 为了防止被锁住的代码块的对象发生变化导致锁失效,通常将需要被锁住的对象设置为final。
Volatile
线程可见性
保证线程可见性
MESI 缓存一致性协议(CPU级别)
禁止指令重排序
禁止指令重排序
DCL单例(Double Check Lock双重检查锁)
问:在双重检查锁的单例模式中要不要加volatile?
答:要加,一般情况下不加volatile结果仍然不会出问题,【
补充知识:
new对象时分为三步:
①给对象申请空间(int类型对象初始设为0)
②给对象的成员变量初始化(int类型对象把需要的值赋给这个对象)
③将空间赋值给新对象(int对象指向空间),此步结束new的对象就不为空了】,但是在如果是int类型,不加volatile进行了指令重排序,那么可能在第一步申请对象空间a=0之后,直接先进行第三步指向a,这样的话在第一步第三步之间第二个线程进来发现对象不为空直接拿走,然后才a=8修改,这样拿到的是0,不是想要的8,加了volatile防止指令重排序,这样指令顺序就不会变了。
loadfence原语指令,storefence原语指令,读写屏障 cpu级别支持的。
注意事项
- volatile不能保证原子性,也就是说volatile不能替代synchronized。
- volatile尽量不要修饰引用类型,引用指向的对象内的成员变量的改变是不会被观察到的。
锁
锁类型
公平锁&非公平锁
公平锁: 在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁: 线程加锁时直接尝试插队获取锁,获取不到就自动到队尾等待。
乐观锁&悲观锁
这两种锁只是一种概念
乐观锁: 乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。实现方式:CAS机制、版本号机制
悲观锁: 悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改。所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。比如Java里面的synchronized关键字的实现就是悲观锁。实现方式:就是加锁、数据库的查询 for update
独享锁&共享锁
这两种锁只是一种概念
独享锁: 是指该锁一次只能被一个线程所持有。例如 Synchronized、ReentrantLock
共享锁: 是指该锁可被多个线程所持有。ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
互斥锁&读写锁
互斥锁和读写锁可以看做是一种实现
互斥锁的实现可以看独享锁;
读写锁也是一种共享锁。
对于Java中的读写锁ReentrantReadWriteLock有以下特性
公平选择性: 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
重进入: 读锁和写锁都支持线程重进入。
锁降级: 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
可重入锁
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对jdk1.7 及以前的ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
锁升级
synchronize是一个升级锁的过程,从无锁->偏向锁->自旋锁(轻量级锁)->重量级锁不断升级的过程
无锁: 顾名思义没有锁住任何对象。
偏向锁
偏向锁: 主要是当前锁经常被同一个线程访问,那么为了让线程获得锁的代价更低,采用了偏向锁。也就是第一次进来的时候不加锁,然后当下次需要获得锁的时候,会偏向于这个已经获取过锁的线程使用。主要是由一个线程单独访问锁时会使用偏向锁,当发生锁竞争时,会升级为自旋锁。
自旋锁
自旋锁: 因为线程的阻塞和唤醒是从用户态转为内核态的,很大程度上会影响性能。因此使用了自旋锁,自旋锁是当锁被其他线程占用时,当前线程并不进入阻塞状态,而是进入一个循环状态,循环过来尝试是否能够获取锁。就没有内核态和用户态的转换,仅仅是CPU调度,很大情况下会减少性能的消耗。当线程自旋超过一定次数后仍然没有获得锁,这个时候线程会被挂起,升级到重量级锁。
适应性自旋锁: 因为如果固定自旋次数,很容易出现线程明明马上就要释放锁,可等待线程超过自旋次数被挂起,所以JDK1.6版本后引入了适应性自旋锁。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
自旋锁是不经过内核态的,所以不占用操作系统资源,始终是在用户态也就是CPU中一直调度。所以耗时更短。
重量级锁
重量级锁: Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。
下面是锁的适用场景
总的来说,轻量级锁适用于线程数少,执行时间快的场景。而线程数量多,执行时间长需要重量级锁。
锁优化
自旋锁(JVM)
自旋锁就是锁优化的一个非常好的成果
锁消除(JVM)
为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于程序员来说这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
锁粗化(JVM/编码)
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
锁细化(编码)
减少锁持有的时间,在一个线程的代码之中,尽量不要对一个方法进行加锁,而是抽离出一个方法中的共享数据,只对方法中的这部分数据代码加锁,这样能减少加锁范围。防止在出现多线程竞争的时候,多个线程对同一个锁进行竞争,而持有锁的代码过长导致执行时间很长,处于阻塞状态的线程等待时间过长,阻塞状态的线程所处理的业务响应速度下降。concurrentHashMap就是明显的一个细化锁的方式。
CAS
CAS定义: cas(Compare And Set) CAS是CPU原语所以能保证不会被打断
cas方法(v, expected, newValue),v代表修改的对象,expected代表期望原本的值,newValue代表改完的新值。
ABA问题
当指向的对象或者内存被修改同时又被改回来时,由于查到的原值并未发生改变(实际上是已经改变并变回来的)这个时候CAS会认为内存或者对象没有发生变化,进行了改值。实际上已经发生了变化
解决办法: 加version(每次改变值都加上一个版本号变化,这样能保证CAS解决ABA问题) AtomicStampedReference 是Java 中专门用来解决ABA问题的类型,实现机制是使用时间戳。
atomic类
Java中以Atomic开头的类都是通过CAS来实现线程安全的
unsafe类
Java中实现cas的类就是unsafe类
- 直接操作内存
- 直接生成类实例
- 直接操作类对象或实例变量