java笔记

ThreadLocal 是线程副本形式,可以保证局部单例,即在各自的线程中是单例的,但是线程与线程之间不保证单例


编写多线程程序有几种实现方式?
    Java 5以前实现多线程有两种实现方法:一种是继承Thread类;
    另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,
    推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活

    实现了Runnable接口的对象并不是线程,它只是任务。Thread对象才是真正的线程创建者。也就是说,任务和线程是分开的,任务放在线程里面才会被执行。

    1. 新建(NEW):新创建了一个线程对象。

    2. 可运行/就绪(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。
        该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

    3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

    4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。
        直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。

        阻塞的情况分三种: 
        (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

        (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

        (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms),或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
                        当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

    5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

    start() run() 

    join() sleep() thread类的方法

    wait() notify() notifyAll()   Object类final+native方法

    等待阻塞--wait()
    同步阻塞--synchronized方法或者同步块
    其他阻塞--sleep方法


start()方法将线程启动并初始化,使线程处于就绪状态。当CPU调度此线程时,就会进入运行状态,执行run()方法。
run()是线程任务的运行者。

通过调用线程类的start()方法来启动一个线程,使线程处于就绪状态,即可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止。

如果直接调用线程类的run()方法,会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程。即start()方法能够异步的调用run()方法,但是直接调用run()方法却是同步的,无法达到多线程的目的。

yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

yield的方法一般不怎么使用,当前线上程调用了yield方法后只是表示此乐意放弃处理器的使用权,调度器可以先去处理其他的程序.但是因为操作系统的调度室不确定的,并且线程是有优先级的,有可能会A线程调用完yield()以后,等会A线程还是会被执行.
    Yield也是一个静态方法和Native。
    Yield告诉当前正在执行的线程为线程池中具有相同优先级的线程提供机会。
    每当一个线程调用java.lang.Thread.yield方法时,它就会向线程调度程序提示它已准备好暂停其执行。线程调度程序可以忽略此提示。
    它只能使一个线程从Running State转为Runnable State,而不是处于wait或blocked状态。
    如果任何线程执行yield方法,则线程调度程序检查是否存在与此线程具有相同或高优先级的线程。如果处理器找到任何具有更高或相同优先级的线程,
        则它将当前线程移动到Ready / Runnable状态并将处理器提供给其他线程,如果不是 ,当前线程将继续执行。
        
join():当A线程执行到了B线程的 .join()方法时,A就好等待,当B线程都执行完,A才会执行。
    很多时候,A线程的输出十分依赖B线程的输入,这个时候A线程就必须等待B线程执行完之后再根据线程B的执行结果进行输出。而JDK正提供了 join() 方法来实现这个功能。
    
    
sleep()方法是线程类(Thread)的静态方法,让调用的线程进入指定时间睡眠状态,使得当前线程进入阻塞状态,告诉系统至少在指定时间内不需要为线程调度器为该线程分配执行时间片,
    给执行机会给其他线程(实际上,调用sleep()方法时并不要求持有任何锁,即sleep()可在任何地方使用。),但是监控状态依然保持,到时后会自动恢复。

    当线程处于上锁时,sleep()方法不会释放对象锁,即睡眠时也持有对象锁。只会让出CPU执行时间片,并不会释放同步资源锁。

    sleep()休眠时间满后,该线程不一定会立即执行,这是因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。 
    sleep()必须捕获异常,在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,
    线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。

    在没有锁的情况下,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

wait()方法   
    wait()方法是Object类里的方法,当一个线程执行wait()方法时,它就进入到一个和该对象相关的等待池中(进入等待队列,也就是阻塞的一种,叫等待阻塞),
    同时释放对象锁,并让出CPU资源,待指定时间结束后返还得到对象锁。

    wait()使用notify()方法、notiftAll()方法或者等待指定时间来唤醒当前等待池中的线程。等待的线程只是被激活,但是必须得再次获得锁才能继续往下执行,
    也就是说只要锁没被释放,原等待线程因为为获取锁仍然无法继续执行。notify的作用只负责唤醒线程,线程被唤醒后有权利重新参与线程的调度。

    wait()方法、notify()方法和notiftAll()方法用于协调多线程对共享数据的存取,所以只能在同步方法或者同步块中使用,否则抛出IllegalMonitorStateException。

    当前线程wait,并不是调用者wait,也就是说this指向的线程进入等待
    当前线程指的是调用t.wait(0)方法的线程,很显然是主线程调用的t.join(),进而调用的t.wait(0),所以join是阻塞主(父)线程,而非子线程。

sleep方法和wait方法的区别:
    (1)属于不同的两个类,sleep()方法是线程类(Thread)的静态方法,wait()方法是Object类里的方法。

    (2)sleep()方法不会释放锁,wait()方法释放对象锁。

    (3)sleep()方法可以在任何地方使用,
        wait()方法则只能在同步方法或同步块中使用(因为wait方法需要释放对象锁,所以只能在同步方法块或者同步代码块)。wait一定是得到当前的线程

    (4)sleep()使线程进入阻塞状态(线程睡眠),wait()方法使线程进入等待队列(线程挂起),也就是阻塞类别不同。


调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,
    调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
    
例如,生产者线程向缓冲区中写入数据,消费者线程从缓冲区中读取数据。消费者线程需要等待直到生产者线程完成一次写入操作。生产者线程需要等待消费者线程完成一次读取操作。
    假设wait(),notify(),notifyAll()方法不需要加锁就能够被调用。此时消费者线程调用wait()正在进入状态变量的等待队列(译者注:可能还未进入)。
在同一时刻,生产者线程调用notify()方法打算向消费者线程通知状态改变。那么此时消费者线程将错过这个通知并一直阻塞。因此,
    对象的wait(),notify(),notifyAll()方法必须在该对象的同步方法或同步代码块中被互斥地调用。

        //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
            public void await() throws InterruptedException { };  
            
        //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
            public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
        
        //将count值减1
        public void countDown() { };  


堆 heap  先进先出

栈 stack 先进后出

start方法会开启一个新的线程执行run方法,所以start方法执行完,不代表run方法执行完

调用sleep()方法会让线程进入睡眠状态---睡眠指定的时间后再次执行;
调用wait()方法会让线程进入等待状态 ----等待别的线程执行notify()或notifyAll()唤醒后继续执行;
调用start()方法会让线程进入就绪状态---得到CPU时间就执行线程;
run()方法是线程的具体逻辑方法,执行完,线程就结束。

线程安全:java内存屏障,内存可见性,happens-before原则  (需要深入学习)


-Djava.compailer=NONE

javap -v 

hsdis  

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*LazySingleton.getInstance

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,'*Volatile.*'


volatile方式的i++,总共是四个步骤:

i++实际为load、Increment、store、Memory Barriers 四个操作。

内存屏障是线程安全的,但是内存屏障之前的指令并不是.

    在某一时刻线程1将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。
    
    如果在此时线程2也执行同样的操作,获取值i==10,自增1变为11,然后马上刷入主内存。
    
    此时由于线程2修改了i的值,实时的线程1中的i==10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。

这四个条件同时满足,就会产生死锁。

    互斥,            共享资源 X 和 Y 只能被一个线程占用;
    
    占有且等待,    线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
    
    不可抢占,        其他线程不能强行抢占线程 T1 占有的资源;
    
    循环等待,        线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
    
    
    

多线程同步和互斥的方法

    用户模式下的方法有: 原子操作( 例如一个单一的全局变量), 临界区。
    内核模式下的方法有: 事件, 信号量, 互斥量


在队列同步器AQS中,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点,

    后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态。
    
    所以为了能让后继节点获取到其前驱节点,同步队列便设置为双向链表,而等待队列没有这样的需求,就为单链表。
    
int waitStatus  所以初始值为0

lock方法:
    compareAndSetState  ->  在unsafe.cpp的 compareAndSwapInt方法     返回的是boolean (jlong)(Atomic::cmpxchg(x, addr, e)) == e;

    tryAcquire        重试、重入的话累加
    
    addWaiter        有head tail节点则将线程加入同步队列中 无则设置head tail节点再将线程加入同步队列
    
    acquireQueued    将当前线程的node传入  用for循环进行自旋 判断线程的prev节点是不是头结点 是的话重试获取锁 成功就将head节点next赋值null  当前线程的node节点设置为head节点
                    
                    将该节点waitStatus设置为signal状态  如果是cancelled状态就去掉  保证当前线程是signal状态  
                    
                    如果线程被中断  将中断状态传递下去
    
    
    LockSupport.park()  用于挂起当前线程,如果许可可用,会立马返回,并消费掉许可


CMPXCHG指令  cmpxchg


interrupt()方法        作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
                        在线程的异常处理中 有线程自己判断是否需要终止
    
interrupted()方法        测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。

isInterrupted()方法    作用是只测试此线程是否被中断 ,不清除中断状态。


countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有
    
    CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue

CountDownLatch
    是一个同步工具类 里面也有一个Sync 继承AQS  它允许一个或者多个线程一直等待 直到其他线程执行完毕  CountDown是倒数的意思  主要是两个方法  countDown方法和await方法
    
    初始化的时候需要制定一个整数  调用await方法的需要用countDown方法倒数  直到state值为0才能继续执行await方法后面的代码
    
    CountDownLatch 是一次性的  CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束

CyclicBarrier    可以用于多线程计算数据,最后合并计算结果的场景。
    
    CyclicBarrier 参与的线程职责是一样的。
    

Semaphore
    semaphore 也就是我们常说的信号灯, semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。
    
    有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就 5 个停车位,每个停车位只能停一辆车,如果这个时候来了 10 辆车,必须要等前面有空的车位才能进入

synchronize和Lock锁的区别
    Lock是一个接口,而synchronized是关键字。
    
    synchronized会自动释放锁,而Lock必须手动释放锁。
    
    Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
    
    通过Lock可以知道线程有没有拿到锁,而synchronized不能。
    
    Lock能提高多个线程读操作的效率。
    
    synchronized能锁住类、方法和代码块,而Lock是块范围内的
    
    

hash冲突的解决方法

    开放寻址法 :线性探测  随机探测
    
    链地址法  : 加一个链表 存储hash值相同的对象
    
    再hash法 : 通过多次的hash值再计算得出一个不同的hash值
    
    溢出区法: 将hash分为基础和溢出 重复的hash值放入溢出区
    


    

ConcurrentHashMap 
    
    不允许key或者value为空
    
    可能出现多个线程的情况
        
        1: 初始化的时候 如果不是当前线程初始化 则 Thread.yield();使当前线程由执行状态,变成为就绪状态 让出cpu时间

put方法 
    
    key || value == null 报错
    
    获取hash值
    
    SIZECTL = -1 在 initTable 方法  表示正在初始化table
    
    MOVED ForwardingNode 的 node 的hash 为 MOVED
    
    循环table
        
        1:如果table == null  
            
            初始化table  initTable()  
                
                使用compareAndSwapInt 操作SIZECTL变量保证只有一个线程初始化table  初始容量没有设置则默认为16
        
        2:如果table不为空并且用tabAt 定位桶的位置  不存在冲突(表示hash值对应的数组下标位置为null) 则直接使用casTabAt进行put操作
        
        
        3:如果key的hash值为move属性 那就进行辅助扩容操作
        
        4:以上条件不满足表示定位桶的位置有值 表示存在冲突
            
            先对当前node元素加锁
                hash值是否大于等于0  
                    
                    是的话 进行链表的循环 如果存在key就替换  不存在就添加到末尾
                    
                    否则探测节点是否为树节点
            
    
    循环table完成put操作之后进行 addCount
        
    addCount:计算hash表存储的元素个数
        
        直接访问baseCount的数量 如果+1成功则表示没有其他线程 然后返回baseCount值
    
        不满足 找到CousnterCell[] 随机的某个下标位置, value=v+x() -> 表示记录元素个数
        
        
        如果前面都是失败, 则进入到fullAndCount();
        
            自旋:
                如果(as = counterCells) != null  
            
                    如果 counterCells 不等于空 表示counterCells已初始化完成
                        
                        用cellsBusy变量 和compareAndSwapInt 进行count数量的累加
            
                    如果cas失败哦,触发CounterCell扩容

                如果 cellsBusy == 0 && counterCells == as && 用cas操作进行 CELLSBUSY 累加 成功表示该线程成功累加count  counterCells记录count值
                
                
                否则进行cas操作累加baseCount
                
        
        判断是否需要扩容  条件 s >= (long)(sc = sizeCtl) && (tab = table) != null &&  (n = tab.length) < MAXIMUM_CAPACITY
            
            s >= (long)(sc = sizeCtl) 超过给定的容量
            
            (tab = table) != null  table为空 则需要扩容
            
            (n = tab.length) < MAXIMUM_CAPACITY  超过最大容量
            
            sc变量判断是主线程扩容还是协助扩容
                
                transfer(tab, null);     null     主线程扩容
                transfer(tab, nt);        nt        协助扩容
            
            
            node扩容一倍 然后从后向前迁移数据

ConcurrentHashMap 高低位迁移
    
    通过  (key的hash值 & table.length-1) 得出在X位为0或者1  从而判断该node节点是高位还是低位  为0位低位 1位高位
    
    好处: table扩容时 低位不迁移  只进行高位的迁移 避免不必要的迁移 提升并发的性能

    例如: tabAt(tab, i = (n - 1) & hash)  所以高低位迁移是使用  n & hash , n = table的长度  hash表示hash值 这样会得出一个0或者1的数值
    
        15 二进制为 0000 1111
        
        31 二进制位 0001 1111        
    
    加入hash为9  则 tabAt = 0000 1001 & 0000 1111 = 0000 1001
    
    当一个node节点为低位  则不需要进行迁移 

& 都为1 则为1 
        


n= 16

16+8+2+1
11011

0000 0000 0000 0000 0000 1000 0001 0000

线程池:
    
    1、线程池的优势
    (1):    降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
    
    (2):    提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
    
    (3):    方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,
            并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
            
    (4):    提供更强大的功能,延时定时线程池。
    

    Executors类提供的四种线程池:
    newCachedThreadPool
        用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
        (可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
        
    newFixedThreadPool
        创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。
        (保证线程数可控,不会造成线程过多,导致系统负载更为严重)
        
    newScheduledThreadPool
        适用于执行延时或者周期性任务。
    
    newSingleThreadExecutor
        创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
        
    Executors创建线程池的弊端

    线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式,

    这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险.
    
    说明:Executors的各个方法的弊端:
    1)newFixedThreadPool和newSingleThreadExecutor:
        主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM.
    
    2)newCachedThreadPool和newScheduledThreadPool:
        主要问题是线程数最多数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM.

        
    并行:指在同一时刻,有多条指令在多个处理器上同时执行;

    并发:指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
        
        
    1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
    
    2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。
    另外,对于无界队列,可忽略该参数。

    3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
    
    4、临时线程存活时间的时间单位

    5、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。

    6、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用newThread()方式,
            threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

    7、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
        拒绝策略
            1、 AbortPolicy:直接抛出异常,默认策略;
            2、 CallerRunsPolicy:用调用者所在的线程来执行任务;
            3、 DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
            4、 DiscardPolicy:直接丢弃任务;
            当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务

    
    HashSet<Worker> workers  线程池容器  


    线程池类型选择

        CPU密集型        保持和CPU核心数一致  不要差太多
    
        IO密集型操作    多设置一些 因为IO可能会阻塞
        
    线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间) /线程 CPU 时间 ) * CPU 数目
    


CAP
    
    一致性(Consistency)、
    可用性(Availability)、
    分区容错性(Partition tolerance)
    
    分区容错性是一定需要的  所以看是选择一致性还是可用性
    

    
1.并行和并发有什么区别?
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
2.线程和进程的区别?
一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
3.创建线程有哪几种方式?
创建线程有三种方式:
继承 Thread 重写 run 方法;
实现 Runnable 接口;
实现 Callable 接口。
4.线程有哪些状态?
线程的状态:
NEW 尚未启动
RUNNABLE 正在执行中
BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
WAITING 永久等待状态
TIMED_WAITING 等待指定的时间重新被唤醒的状态
TERMINATED 执行完成
5.sleep() 和 wait() 有什么区别?
类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

1. notify()和 notifyAll()有什么区别?
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
2. 线程的 run() 和 start() 有什么区别?
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
3. 在 Java 程序中怎么保证多线程的运行安全?
· 方法一:使用安全类,比如 Java. util. concurrent 下的类。
· 方法二:使用自动锁 synchronized。
· 方法三:使用手动锁 Lock。
手动锁 Java 示例代码如下:
Lock lock = new ReentrantLock();
lock. lock();
try {
    System. out. println("获得锁");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System. out. println("释放锁");
    lock. unlock();
}
4. 多线程中 synchronized 锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
5. 什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。


1. 怎么防止死锁?
· 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
· 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
· 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
· 尽量减少同步的代码块。
2. ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
3. 说一下 synchronized 底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
4. synchronized 和 volatile 的区别是什么?
· volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
· volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
· volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
5. synchronized 和 Lock 有什么区别?
· synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
· synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
· 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
55. synchronized 和 ReentrantLock 区别是什么?
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下:
· ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
· ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
· ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
· volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值