线程安全
什么是线程安全呢?为什么要保证线程安全?
线程安全是指先多个线程访问同一资源(数据)进行修改时,要保证数据的一致性。这也叫保证数据的一致性。
为什么要保证线程安全呢?
举个例子,现在我们定义了一个整形数据100,此时我们定义两个线程,线程A和线程B,都对它进行+1操作,请问结果是多少呢?不清楚,有可能是101,也有可能是102,因为我们不清楚这个线程是什么时候开始,当它拿到数据时到底是100,还是101,此时线程进行操作就会有意想不到的后果了。
所以当我们有多个线程操作同一资源时,我们必须要有同步的操作,要保证数据的一致性,让资源在同一时刻只有一个线程在进行操作。
我们将同一时刻只能有一个线程访问的资源叫做临界资源。
将临界资源的代码段,叫做临界区
临界区特点:某一时刻如果有一个线程正在访问代码段,其他线程想要访问,只有等待当前的线程离开该代码段,你才可以访问,这样保证了线程安全。
Synchronized
为了保证我们的线程安全,引入了Synchronized关键字。
Synchronized关键字中放入代码段或者定义成方法。此时这个方法就定义为同步方法。
(tips:单独对某一行代码定义为同步代码没有任何意义)
例如:
两个线程都顺序打印数字,一个线程答应1-5,另一个线程答应6-10
(如果不对线程访问加锁,是无法做到的)
public static void main(String[] args) {
ThreadTestA A = new ThreadTestA();
ThreadTestA B = new ThreadTestA();
new Thread(A).start();
new Thread(B).start();
}
class ThreadTestA implements Runnable {
private static int number = 1;
@Override
public void run() {
//较好系统访问的是类信息number,我们可以对这个类加锁
synchronized(ThreadTestA.class){
for(int i = 0; i < 5;i++){
System.out.println(Thread.currentThread().getName()+"打印"+(number++));
}
}
}
}
结果如下:
又或者我们对某一个方法进行加锁
class ThreadTestA implements Runnable {
private static int number = 1;
public synchronized void prints(){
for(int i = 0; i < 5;i++){
System.out.println(Thread.currentThread().getName()+"打印"+(number++));
}
}
@Override
public void run() {
prints();
}
}
此时就有人吹毛求疵,这把锁太重了,范围太大了。那么我们就对方法体加锁,Synchronized(对象),这个对象可以是类中的Object。此时我们获取这个Object的锁也就相当于获得了对象的锁,因为这个Object就在这个对象中,所以就相当于要获得这个ThreadTestA类的实例对象的锁。
class ThreadTestA implements Runnable {
private static int number = 1;
Object object = new Object();
public void prints(){
synchronized(object) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "打印" + (number++));
}
}
}
@Override
public void run() {
prints();
}
}
底层如何实现锁的获取
那么Java的底层是如何获取到锁的呢?获取到什么锁?
首先我们要理解一个获得锁这个概念,不是获取代码段,或者方法的锁,而是获取对象的锁。
所有的对象天生就有一把锁,而这把锁叫做monitor锁,也就是监视器锁。
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
执行monitorexit的线程必须是object ref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
从反编译的结果来看,方法的同步并没有通过指令 monitorenter 和monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过同步方法相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
Java对象头中的markword以及锁升级
首先我们了解下对象头的结构
Java对象头的结构分别是:markword,类型指针。
一个数组的对象的对象头是:markword,类型指针,数组长度。
JVM中的markword图如下
我们能看到锁并不是一次就让其他线程阻塞的,因为让其他线程阻塞是需要OS(操作系统的调度),如果线程太多,那么系统的调度就会很慢了,这与我们想要的快速不符。所以我们在中间进行过度。
那么我们就来了解一下Synchronized锁的升级吧。
概述:
无锁:现在这个对象没有任何线程占用它,所以不需要上锁,先来先得。
偏向锁:此时线程A要获取锁,一看没有人占用这把锁,那就拿来用,此时这把锁被A标记,这就是我A的锁,锁升级为偏向锁,偏向A。下次A再获得锁的时候,直接拿就行了,锁已经被A标记了,
此时线程B也想要锁,但是现在锁被A标记了,那不行B就抢(通过CAS的方式),抢成功了。那么锁就被B标记,此时还是偏向锁,但是偏向了B,下次B来的时候就不用抢了,这就B的了;抢失败了,现在锁发现有人再抢自己,锁烦死了,直接升级为轻量级锁。
轻量级锁:B抢失败之后,B就想那不行,不能放弃,B再抢(此时锁变成为自旋锁),抢了10次!此时发现B还抢这把锁,锁开始恼怒了,你不能再抢我了,你给我原地站着。此时轻量级锁升级为重量级锁,由OS调度。
重量级锁:此时线程A获取到锁,其他线程,例如线程B,直接阻塞。
详细:
无锁:当这个对象Object的对象头中markword的锁标志位位01,并且偏向锁的标记为0,那就证明此时的锁状态就是无锁。此时线程先来先得,谁先拿到Object的锁,这个锁就升级为偏向锁,并且偏向它,偏向的线程用线程ID号来保存。
偏向锁:现在线程A首先抢到了Object的锁使用权,此时锁的偏向锁的状态为置为1,线程ID保存的就是线程A的ID。A线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。
此时线程B的也要抢锁的使用权,就使用CAS尝试抢夺锁。抢夺成功,那就直接把线程ID改为自己的(B的)线程ID;抢夺失败,此时锁发现自己在被其他线程争抢,升级为轻量级锁。
轻量级锁:锁标志位置为00.当锁升级为轻量级锁的时候,线程B还在不断的抢夺(抢夺的方法还是CAS)。那么锁就变成自旋锁,(JDK1.6版本是B再抢夺10次),当自旋次数内A释放了该锁的资源,那么此时B就进去占用,如果此时B没结束,线程A又来了,接着进入自旋状态抢夺锁,一直到自旋锁结束,或者B释放锁的资源。
此时自选锁结束了,但是还是没抢到锁的资源。那就升级为重量级锁,让老大哥OS去操作。
重量级锁:锁标志位置为10。此时线程A和线程B还有线程C要获取锁的对象,由OS去调度,A获取了,那么B和C就阻塞,等待A的释放,此时A释放了,再由OS调度,B和C谁来获得锁,其他线程都阻塞。