java—线程锁

Java多线程的锁都是基于对象的。

Java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊的Java对象。所以我们常说的类锁,其实就是Class对象的锁。

synchronized :同步。java线程锁关键字。

通过synchronized关键字加锁主要有三种形式:

public class Sync {
    
    

    public final static Object lock = new Object();

    //锁为当前对象
    public synchronized void doSomething(String a){
    
    
        System.out.println("锁为当前实例============");
        //等效于这样加锁
   /* synchronized (this){
    }*/
    }

    //锁为Class对象
    public static synchronized void doSomething(Integer a){
    
    
        System.out.println("锁为当前类的Class对象===========");
    }

    //锁为创建的对象lock
    public void doSomething(Long a){
    
    
        synchronized (lock){
    
    
            System.out.println("锁为lock指向的对象===========");
        }
    }
}

Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在Java 6 以前,所有的锁都是”重量级“锁。所以在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

1、无锁状态

对象被创建出来,没有线程访问。

2、偏向锁状态

锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。偏向锁会偏向于第一个访问的线程,如果在接下来的运行过程中,该锁没有被其他线程访问,则持有偏向锁的线程永远不会触发同步。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程ID。当下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己 的线程ID。如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,这个时候要分两种情况:

  • 成功,表示之前的线程不存在了, Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

3、轻量级锁状态

多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。

4、重量级锁状态

如果一个线程已经获取到锁,另一个线程去获取锁时会获取失败,此时会自旋尝试获取锁,当自悬到一定程度(和JVM操作系统有关)还没有获取到锁,则该线程会阻塞(需要唤醒,不占用CPU资源,自旋时会消耗CPU资源)。同时这个锁升级为重量级锁。

当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁

new,无线程访问(无锁) —— 偏向锁(一个线程) —— 轻量级 (少量线程竞争)—— 重量级锁 (竞争的锁太多,竞争太多次)

锁升级流程:

每一个线程在准备获取共享资源时:

**第一步,**检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

**第二步,**如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

**第三步,**两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。

**第四步,**第三步中成功执行CAS的获得资源,失败的则进入自旋 。

**第五步,**自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

**第六步,**进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

锁的等级只允许升级,但是在HotSpot JVM支持锁降级,锁降级发生在“Stop-The-World”期间。

各种锁的优缺点对比:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行时间较长。

锁的几种分类:

1,可重入锁和非可重入锁

​ 可重入锁:又称递归锁,支持一个线程在同步代码块再次自动获取该锁。不会造成死锁。synchronized关键字就是可重入锁。

2、公平锁与非公平锁

​ 公平锁是先竞争锁的线程获取到锁,后获竞争的线程后获取锁,先到先得。FIFO(First Input First Output)原则。反之就是非公平锁。

​ 一般情况下,非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有一些线程长时间得不到锁)的情况

3,读写锁和排它锁

​ 读写锁:允许多个线程访问。写锁只允许同时一个线程获取,且写锁被线程获取,读锁不能被获取,读锁被获取时,写锁不能被获取。读锁可以多个线程同时获取。ReentrantReadWriteLock

​ 排它锁:只允许一个线程访问,synchronized 和 ReentrantLock (java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁)都是排它锁

使用ReentrantLock 上锁(代替synchronized )

public class MyReentrantLock {
    
    

    /**
     * 从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
     * 我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
     * java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
     */
    public static final Lock lock = new ReentrantLock();// Lock.newCondition()

    /**
     * 实现 线程等待wait() : condition.await(); , 唤醒线程notify() : condition.signal();
     */
    public static final Condition condition = lock.newCondition();

    public static int count = 0;

    /**
     * 获取锁 释放锁
     * @param name
     */
    public static void add(String name){
    
    
        System.out.println(name);
        //获取锁
        lock.lock();
        try {
    
    
            count ++;
            //System.out.println(count);
            System.out.println("===" + name + "====:" + count);
        }finally {
    
    
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 尝试获取锁,获取不到去做别的
     */
    public static void tryLock(String name){
    
    
        try {
    
    
            //lock.tryLock 线程尝试获取锁,2s后未获取到返回false,不会一直等待
            if(lock.tryLock(2, TimeUnit.SECONDS)){
    
    
                Thread.sleep(2 * 1000);
                try {
    
    
                    System.out.println("线程:" + name + "获取到锁");
                }finally {
    
    
                    lock.unlock();
                }
            }else {
    
    
                System.out.println("===线程:" + name + "未获取到锁");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
    
    

        //获取锁 释放锁
        /*for(int i = 0;i < 10 ;i ++){
            int finalI = i;
            new Thread(() -> {
                MyReentrantLock.add("thread-" + finalI);
            }).start();
        }*/

        //尝试获取锁
        for(int i = 0;i < 15 ;i ++){
    
    
            int finalI = i;
            new Thread(() -> {
    
    
                MyReentrantLock.tryLock("thread-" + finalI);
            }).start();
        }
    }
}

使用读写锁ReentrantReadWriteLock

public class MyReadWriteLock {
    
    

    /**
     * ReadWriteLock
     * 只允许一个线程写入(其他线程既不能写入也不能读取);
     * 没有写入时,多个线程允许同时读(提高性能)。
     */
    /**
     * ReadWriteLock 实例 static final 确保锁唯一 ,引用不可变
     */
    private static final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    /**
     * 读锁
     */
    private static final Lock rLock = rwLock.readLock();

    /**
     * 写锁
     */
    private static final Lock wLock = rwLock.writeLock();

    private static int i = 0;

    /**
     * 写操作
     * @param name
     */
    public static void wirte(String name){
    
    
        //上锁
        wLock.lock();
        try {
    
    
            System.out.println("线程" + name + "写操作===========");
            i ++;
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            //释放锁
            wLock.unlock();
        }
    }

    /**
     * 读操作
     * @param name
     */
    public static void read(String name){
    
    
        //上锁
        rLock.lock();
        try {
    
    
            if("sleep".equals(name)){
    
    
                Thread.sleep(10 * 1000);
            }
            System.out.println("读线程=" + name + "读取:" + i);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //释放锁
            rLock.unlock();
        }
    }

    public static void main(String[] args) throws Exception{
    
    
        //线程获取到读锁,写锁不能被获取
        /*new Thread(() -> MyReadWriteLock.read("sleep")).start();
        Thread.sleep(1000);*/
        //进行1000次写操作
        for(int i = 0; i < 1000; i ++){
    
    
            int finalI = i;
            new Thread(() -> MyReadWriteLock.wirte("写线程" + finalI)).start();
        }
        //读取到的 i 会为1000,说明写锁是排它锁,只允许同一时间一个线程获取写锁
        for(int i = 0 ; i < 100; i ++){
    
    
            final int a = i;
            new Thread(() -> MyReadWriteLock.read("读线程==" + a)).start();
        }
    }
}

使用StampedLock :把读锁分为了“乐观读锁”和“悲观读锁”两种

public class MyStampedLock {
    
    

    /**
     * 读的过程中也允许获取写锁后写入 ,乐观锁
     */
    private final static StampedLock stampedLock = new StampedLock();

    private static int i = 0;

    public static void add(String a){
    
    
        //获取写锁
        long stamp = stampedLock.writeLock();
        try {
    
    

            i ++;
            System.out.println("线程:" + a + ",在写:" + i);
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            //释放写锁
            stampedLock.unlockWrite(stamp);
        }
    }

    public static void read(String name){
    
    
        long code = System.currentTimeMillis();
        //获取一个乐观读锁
        long stamp = stampedLock.tryOptimisticRead();
        int a = i;
        System.out.println(code + ";线程" + name + "预读取:" + a);
        try {
    
    
            Thread.sleep(300);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //检查是否发生了写锁
        if(!stampedLock.validate(stamp)){
    
    
            System.out.println(code +";线程" + name + "重新读取中=========");
            //发送写操作,获取普通读锁
            stamp = stampedLock.readLock();//获取一个悲观读锁
            try {
    
    
                a = i;
                System.out.println(code  + ";线程" + name + "重新读取取到:" + a);
            }finally {
    
    
                stampedLock.unlockRead(stamp);//释放读锁
            }
        }else {
    
    
            //要读取时发生了写入操作
            System.out.println("发生了写入======:" + name);
        }
    }

    public static void main(String[] args) {
    
    
        for(int i = 0;i < 2 ;i ++){
    
    
            final int a = i;
            new Thread(() -> {
    
    
                try {
    
    
                    //一直循环写入,两个线程
                    while (true){
    
    
                        Thread.sleep(500);
                        MyStampedLock.add("thread" + a);
                    }
                }catch (Exception e){
    
    
                    e.printStackTrace();
                }

            }).start();
        }
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        for(int i = 0;i < 5; i ++){
    
    
            final int a = i;
            new Thread(() -> {
    
    
                //循环一直读取
                while (true){
    
    
                    try {
    
    
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    MyStampedLock.read("read" + a);
                }
            }).start();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/zhaoqingquanajax/article/details/112382586
今日推荐