synchronized的可重入特性
1 什么是可重入(递归锁):
指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。类车牌照摇号,可重入就是拿到之后就可以再次用起来,可以一直摇号,如果还需要重新竞争的话就叫不可重入锁
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁
2 可重入有什么好处:
避免死锁:如果不可重入就会一直等待锁,很容易造成死锁
提升封装性:同步代码块可以调用另一个同步代码块,方便通过方法封装
synchronized的不可中断
不可中断性指:一旦我需要的锁被别的线程获得,我只能选择阻塞等待,直到别的线程释放这个锁,如果别人永远不释放,我只能永远等下去
package com.synconized;
import org.apache.log4j.Logger;
import java.util.concurrent.TimeUnit;
/**
目标:演示synchronized不可中断
1. 定义一个Runnable
2. 在Runnable定义同步代码块
3. 先开启一个线程来执行同步代码块,保证不退出同步代码块
4. 后开启一个线程来执行同步代码块(阻塞状态)
5. 停止第二个线程
*/
public class Demo6 {
private static Object obj = new Object();//定义锁对象
static Logger log = Logger.getLogger(Demo6.class);
public static void main(String[] args){
// 1. 定义一个Runnable
Runnable task = ()->{
// 2. 在Runnable定义同步代码块;
// 同步代码块需要一个锁对象;
synchronized(obj){
// 进行打印是哪一个线程进入的同步代码块
String name = Thread.currentThread().getName();
// 为了让t1不释放锁,让其自己循环(自旋)
while (true){
log.debug(name + "进入同步代码块,运行...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*try {
TimeUnit.SECONDS.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
};
// 3. 先开启一个线程来执行同步代码块
Thread t1 = new Thread(task);
t1.start();
// 睡眠一秒钟;保证第一个线程先去执行同步代码块之后再来创建第二个线程;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
t2线程执行同步代码块会进入阻塞状态
锁已经被t1线程锁获取得到了;
所以t2是无法获取得到Object obj对象锁的;
那么也就将会在同步代码块外处于阻塞状态。
*/
Thread t2 = new Thread(task);
t2.start();
/**
* 为了看到t2因获取不到锁,进入等待,延迟一秒钟
*/
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t2的状态: "+t2.getState());
/**
* 尝试中断处于等待的t2线程,观察t2线程是否能够被中断
*/
log.debug("停止t2线程前");
t2.interrupt();//中断
// t2.stop(); //也可以试一下停止(虽然stop已经过期,但也可以用)
log.debug("停止t2线程后");
// 最后得到两个线程的执行状态
log.debug("t1的状态"+t1.getState());
log.debug("t2的状态"+t2.getState());
}
}
synchronized优化
CAS 全称是Compare-And-Swap,它是一条CPU并发原语,它的功能是判断内存某个位置是否为预期值如果是则更改为新的值,这个过程是原子的
Unsafe类介绍
java是无法去操作内存地址的(即没有指针);
Unsafe类使java拥有了像C语言的指针一样操作内存空间的能力(操作对象的内存空间即能够操作对象里面的内容;但是这个UnSafe类不太安全;如果使用不当会出现一些比较危险的事情;所以java官方并不推荐使用;并且在jdk当中也无法找到此类;只能够通过反射的方式才能够找到该类),同时也带来了指针的问题。
过渡的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
Unsafe对象不能直接调用,只能通过反射获得。
package com.synconized;
import java.util.concurrent.atomic.AtomicInteger;
public class TestCas {
public static void main(String[] args) {
//本质是CAS,可以看做一个高级的Integer
AtomicInteger atomicInteger = new AtomicInteger(0);
new Thread(()->{
//compareAndSet(0,1)从0修改为1(CAS操作)
System.out.println(atomicInteger.compareAndSet(0, 1)+"-线程"+Thread.currentThread().getName()+"-当前的值为:"+atomicInteger.get());
},"t1").start();
new Thread(()->{
System.out.println(atomicInteger.compareAndSet(0, 1)+"-线程"+Thread.currentThread().getName()+"-当前的值为:"+atomicInteger.get());
},"t2").start();
}
}
cas原子操作过程
两个线程都想修改主物理内存的值,调用compareAndSet()函数
线程1拿到主物理内存的0然后拷贝一个副本放在自己这然后把这个副本与期望值相比发现相等所以返回true并将0改成1写回主物理内存并通知其他线程可见,
线程2再去拿到主物理内存的值1作为拷贝副本将这个副本与实际的期望值相比不是0所以返回false主物理内存的值也无法得到有效更改。
/**
*
* @param expect 原值
* @param update 修改之后的值
* @return
*/
public final boolean compareAndSet(int expect, int update) {
/*this: 当前对象
valueOffset: 当前对象的内存地址
*/
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
它会调用unsafe类的compareAndSwapInt方法CAS能保证原子性靠的就是底层的unsafe类这个东西是JDK自带的,
在home\jdk\jre\lib\rt.jar里面在sun.misc这个包下。
CAS是JAVA的核心类,JAVA不能直接和操作系统打交道,需要靠本地方法的帮助,其中的方法可以向C的指针一样直接操作内存。
unsafe类的几乎所有方法都是native修饰的,直接调用底层资源执行相应任务。这个方法会拿到当前对象和当前对象的地址,然后修改。
CAS缺点
1.首先我如果一个自增的方法执行时,有个do while如果CAS失败他就会一直尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2.CAS只能保证一个共享变量的原子性,对多个共享变量的原子性往往无能为力,这时候就必须用锁来保证它的原子性。
3. 他会引发ABA问题
synchronized 加锁的三种方式
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
public synchronized void test(){
System.out.println("修饰实例方法");
}
修饰静态方法,作用于当前类字节码对象加锁,进入同步代码前要获得当前类对象的锁
public static synchronized void test2(){
System.out.println("修饰静态方法");
}
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
public void test3(){
synchronized (this){
System.out.println("修饰代码块");
}
}