java锁

文章目录

  • java锁
    • 可重入锁及其验证
    • 手写一个自旋锁
    • 读写锁代码验证
    • CountDownLatch
    • CyclicBarrier
    • Semaphore
    • synchronized和ReentrantLock区别
    • 死锁

java锁

  • 公平锁:多个线程按照申请锁的顺序获取锁,先来后到,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则加入到等待队列中,遵从FIFO。
  • 非公平锁:多个线程获取锁的顺序不一定是申请锁的顺序,高并发情况下可能会优先级反转、出现饥饿现象(某些线程一直等待)。每个线程上来就尝试占有锁,如果失败,就采用类似公平锁的方式。优点是吞吐量大,synchronized也是非公平锁
  • 不可重入锁(递归锁):同一线程,执行某个方法已经获取了该锁,那么在该方法中再次获取该锁(同一线程)就会获取不到而阻塞
  • 可重入锁:同一线程外层函数获得锁后,内层函数可自动加锁 (前提锁对象得是同一个对象或者class)。且线程可以进入它已经拥有的锁的同步代码块儿。synchronized和ReentrantLock都是可重入锁,可重入锁最大的作用是避免一定程度的死锁。
  • 自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样做的好处是减少线程上下文的切换消耗,缺点是循环会消耗CPU。
  • 独占锁:该锁只能被一个线程所持有,ReentrantLock和synchronized都是独占锁。
  • 共享锁:该锁可以被多个线程所持有。ReentrantReadWriteLock而言,读锁是共享锁,写锁是独占锁。读锁是共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的,只要包含写的操作都只有一个线程能够占有。

可重入锁及其验证

class Duck {
    private synchronized void swim() {
        System.out.println(Thread.currentThread().getName() + "游泳游泳");
        sound();
    }

    private synchronized void sound() {
        System.out.println(Thread.currentThread().getName() + "叫唤");
    }
}

class Bird {

    Lock mLock = new ReentrantLock();

    void fly() {
        mLock.lock();
        System.out.println(Thread.currentThread().getName() + "fly fly~~");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        eat();
        mLock.unlock();
    }

    private void eat() {
        mLock.lock();
        System.out.println(Thread.currentThread().getName() + "eat eat~~");
        mLock.unlock();
    }
}

public class LockTest {
    //可重入锁
    public static void main(String args[]) {
        Duck duck = new Duck();
        new Thread(duck::swim, "t1").start();
        new Thread(duck::swim, "t2").start();

        Bird bird = new Bird();
        new Thread(bird::fly, "t3").start();
        new Thread(bird::fly, "t4").start();
    }
}

t1游泳游泳
t1叫唤
t2游泳游泳
t2叫唤
t3fly fly~~
t3eat eat~~
t4fly fly~~
t4eat eat~~

Process finished with exit code 0

值得注意的是,使用Lock后,一段代码可以重复进行加锁,但加锁后一定要匹配释放锁,否则少一个释放锁线程不会结束

手写一个自旋锁

public class MySpinLock {

    private AtomicReference<Thread> mThreadAtomicReference = new AtomicReference<>();

    private volatile AtomicInteger version;
    private volatile AtomicInteger count;

    public static void main(String[] args) throws InterruptedException {
        spinLock1();
    }

    private static void spinLock1() throws InterruptedException {
        MySpinLock mySpinLock = new MySpinLock();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            mySpinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mySpinLock.myUnlock();
            }
        }, "t1").start();

        new Thread(() -> {
            mySpinLock.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mySpinLock.myUnlock();
            }
        }, "t2").start();
    }

    private void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "lock");
        while (!mThreadAtomicReference.compareAndSet(null, thread)) {

        }
    }

    private void myUnlock() {
        Thread thread = Thread.currentThread();
        mThreadAtomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + "unlock");
    }
}

读写锁代码验证

读写锁,只要包含写操作都应该是独占的,读与读之间可以共享。写的时候加写锁,必须保证线程互斥,不能有其他线程干扰,读的时候可以共享,所以是共享锁。

也就是说,读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占读写锁的。当有读锁时,写锁就不能获得;而当有写锁时,除了已经获得写锁的这个线程外,其他线程不能获取该读写锁的任何读锁。

读写锁升降级:

  1. 锁升级:读锁在没有释放的情况下,就去申请写锁,属于锁升级,ReentrantReadWriteLock不支持
  2. 锁降级:写锁在没有释放的情况下,就去申请读锁,属于锁降级,ReentrantReadWriteLock支持。需要注意的是,从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
//资源类
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在写入");
            try {
                Thread.sleep(300);
            } catch (Exception e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    //读共享且时间段不一样
    public void get(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在读取");
            try {
                Thread.sleep(300);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Object res = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完成:" + res);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

public class MyReadWriteLock {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.put(finalI + "", finalI);
            }, "t" + i).start();
        }
        for (int i = 5; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.get((finalI - 5) + "");
            }, "t" + i).start();
        }
    }
}

CountDownLatch

非同步代码:

public class CountDownLatchTest {
    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
                }
            },"t" + i).start();
        }


        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
            }
        },"班长").start();
    }
}

t1上完自习走人,离开教室
t0上完自习走人,离开教室
t2上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
班长班长上完自习走人,锁上门窗,离开教室
t3上完自习走人,离开教室

Process finished with exit code 0

使用CountDownLatch进行计数同步后,前六个线程执行完(顺序随机)才能执行被CountDownLatch锁住的线程:

public class CountDownLatchTest {
    public static void main(String[] args) {

        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
                countDownLatch.countDown();
            },"t" + i).start();
        }

        new Thread(() -> {
            try {
                countDownLatch.await();
                System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"班长").start();
    }
}

t0上完自习走人,离开教室
t3上完自习走人,离开教室
t2上完自习走人,离开教室
t1上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
班长班长上完自习走人,锁上门窗,离开教室

Process finished with exit code 0

CyclicBarrier

同理,和CountDownLatch类似,不过它是做加法:

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6, new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
            }
        });

        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
                    cyclicBarrier.await();//在此阻塞班长线程
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"t" + i).start();
        }
    }
}

t1上完自习走人,离开教室
t0上完自习走人,离开教室
t3上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
t2上完自习走人,离开教室
t2班长上完自习走人,锁上门窗,离开教室

Process finished with exit code 0

Semaphore

抢车位,车位资源有限,先抢到先停,停满了后面的排队等里面的释放。

public class SemaphoreTest {

    public static void main(String[] args) {
        //模拟3个停车位
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "抢到车位");
                        Thread.sleep(300);
                        System.out.println(Thread.currentThread().getName() + "停车后离开");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                }
            }, "t" + i).start();
        }
    }
}

t0抢到车位
t2抢到车位
t1抢到车位
t0停车后离开
t1停车后离开
t2停车后离开
t3抢到车位
t4抢到车位
t5抢到车位
t3停车后离开
t5停车后离开
t4停车后离开

Process finished with exit code 0

synchronized和ReentrantLock区别

  1. 原始构成:
    • sync是java关键字,属于JVM层面。使用java p编译后可以看到机器码中是monitorenter和monitorexit,其底层是通过monitor实现的,其实wait和notify等方法也依赖于monitor对象,只有在同步代码块中才能被调用。
    • lock是具体的类,属于API层面。
  2. 使用方法:
    • sync不需要手动释放锁,代码块执行完成后系统会自动让线程释放锁,且出现异常 不会退出失败,汇编码可以看到monitor其实退出了两次,就算是有异常发生也能退出。
    • lock需要手动释放锁,若没有主动释放锁,线程一直运行下去,可能会造成死锁,需要lock,unlock,try,catch,finally语句块来完成
  3. 等待中断:
    • sync,如果thread1不释放锁,thread2将一直阻塞等待下去,不能被中断,除非thread1抛出异常或运行完成。(1.6之后引入自旋等技术可能会好点)
    • lock, 可中断,设置超时方法tryLock(long timeout, TimeUnit unit)或者lockInterruptible()放代码块中,调用interrupt()可中断等待。
  4. 加锁是否公平:
    • sync只可能是非公平锁
    • lock可以通过构造方法指定,默认是非公平
  5. 多个Condition
    • sync不能绑定多个条件
    • lock可以实现分组唤醒需要唤醒的线程,可以精确唤醒,不像是sync要么随机唤醒(notify)一个要么全部唤醒(notifyAll)。
  6. 性能
    • lock底层实现使用了自旋CAS,可以很好地避免使线程由用户态转为内核态,减少线程上下文消耗,且灵活性较高,但使用起来没有sync方便。
    • sync在1.6之前性能较lock有很大不足,1.6之后引入了自旋、偏量锁等,性能几乎持平,两者都可以用的情况下官方推荐sync,并且JVM未来发展更倾向于sync,阅读1.8的源码可以发现,很多地方已经由lock改成sync,如写时复制的List、map、set等(CopyOnWriteArrayList)。而sync使用起来更为方便,但是唤醒操作单一随机,没有lock那么灵活,所以具体使用哪个还是要看场景。

死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都无法推进下去,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的概率就很低。

java锁_第1张图片

代码验证:

class HoldLockThread implements Runnable {
    private String lockA;
    private String lockB;

    public HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + ", 自己持有" + lockA + "尝试获得" + lockB);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + ", 自己持有" + lockB + "尝试获得" + lockA);
            }
        }
    }
}

public class DeadLockTest {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new HoldLockThread(lockA, lockB)).start();
        new Thread(new HoldLockThread(lockB, lockA)).start();
    }
}

死锁排查:

jps -l查看所有java进程

找到当前程序的进程号:jstack {进程号},就可以看到栈信息,有没有死锁了。

解决办法:重启程序、优化代码,避免出现死锁。

你可能感兴趣的:(java线程安全,java,多线程,并发编程)