juc并发编程入门(二)

60 篇文章 1 订阅

接下来我们来看看多线程锁的理解

悲观锁:适合写操作多的场景,先加锁可以保证写操作时数据正确,效率低,会造成阻塞;

乐观锁:适合读操作多的场景,使得读操作性能提升,采用版本号,和cas(比较并交换)来实现;

高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁;

尽可能使得加锁的代码块工作量小,避免在锁代码块中调用RPC方法;

synchronized:同步锁,悲观锁,独占锁,只要在一个方法加了这个synchronized,那么整个类中其他的加synchronized的方法都会被锁住,因为他是独占的啊,不能让别人抢我的资源,千万不要认为他锁的是这个方法,同一时间内,只能有一个加锁的方法被访问

类锁就相当于你家里的大门锁,而对象锁就是你屋子那把小锁;

在方面上面了加了静态同步锁,那么就是类锁;

在方面上了加了普通的同步锁,那么就是对象锁;

2个同步锁方法,谁先调用?

public class Producer {

    public synchronized void aa(){
        System.out.println("aa:"+Thread.currentThread().getName());
    }
    public synchronized void bb(){
        System.out.println("bb:"+Thread.currentThread().getName());
    }
    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        new Thread(()->{
            producer.aa();
        }).start();

        new Thread(()->{
            producer.bb();
        }).start();

        System.out.println("main结束了");
    }
}

我们可以看到aa先执行,bb在执行

我们给aa加3秒的延迟,在来看看谁先执行

可以看到还是aa先执行,然后再是bb,为什么呢?

因为aa和bb他们拿到的是同一把对象锁,也就是producer这个对象

我们可以知道加了同步锁的方法,在当前类中所有被加同步锁的方法,只能有一个被访问到

所以aa先执行,bb在执行

这次我们把aa和没有加锁的cc方法比较,看看谁先调用


public class Producer {

    public synchronized void aa(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("aa:"+Thread.currentThread().getName());
    }
    public synchronized void bb(){
        System.out.println("bb:"+Thread.currentThread().getName());
    }

    public void cc(){
        System.out.println("cc:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        new Thread(()->{
            producer.aa();
        }).start();

        new Thread(()->{
            producer.cc();
        }).start();

        System.out.println("main结束了");
    }
}

可以看到没有加锁的cc先调用,然后再是aa,为什么呢?

因为cc没有加锁,不会和aa的锁抢占资源,所以cc先执行

我们在看一下2个不同的对象,分别调用aa和bb方法

可以看到,bb先执行,aa在执行,为什么呢?

因为aa和bb的对象锁不是同一把,所以不存在互相争抢资源的现象,所以bb先执行

接下来我们在看下,有2个静态同步锁方法,谁先打印

public class Producer {

    public static synchronized void aa(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("aa:"+Thread.currentThread().getName());
    }
    public static synchronized void bb(){
        System.out.println("bb:"+Thread.currentThread().getName());
    }

    public void cc(){
        System.out.println("cc:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        new Thread(()->{
            producer.aa();
        }).start();

        new Thread(()->{
            producer.bb();
        }).start();

        System.out.println("main结束了");
    }
}

可以看到还是先打印了aa,然后再打印bb

我们在看下2个不同的对象分别调用aa和bb,谁先打印

可以看到还是aa先打印,然后再是bb,为什么呢?

在静态同步锁方法中,不管你是几个对象,他拿的都是同一把锁,并且静态方法的调用

不需要对象,静态方法,不管你new了多少个对象,他拿到的类模版都是同一个

所以aa先打印,然后再是bb

接下来我们在来看下静态同步锁方法和普通同步锁方法谁先调用

public class Producer {

    public static synchronized void aa(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("aa:"+Thread.currentThread().getName());
    }
    public synchronized void bb(){
        System.out.println("bb:"+Thread.currentThread().getName());
    }

    public void cc(){
        System.out.println("cc:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
       // Producer producer2=new Producer();
        new Thread(()->{
            producer.aa();
        }).start();

        new Thread(()->{
            producer.bb();
        }).start();

        System.out.println("main结束了");
    }
}

可以看到是普通同步锁方法先调用

可以看到bb先执行,然后才是aa,为什么呢?

因为bb是对象锁,而aa他是类锁,静态方法不管你new了多少个对象,他拿到的类模版都是同一个,aa和bb他们不是同一把锁,不存在互相争抢资源的情况,所以bb先执行

我们上面说过,能用对象锁,就不要使用类锁

我们在来看下分别使用2个不同的对象调用1个静态同步锁方法,1个普通同步锁方法

可以看到还是bb先执行,为啥呢?

因为aa是类锁,不管你new了多少个对象,拿到的类模版都是同一个,

所以aa和bb不存在竞争资源的问题,不是同一把锁;

bb是对象锁,所以bb先执行,所以能用对象锁,就不要使用类锁

接下来我们通过字节码指令的方式来看下同步锁

public class Producer {

    public Object object=new Object();

    public void aa(){
        synchronized (object){
            System.out.println("同步代码块");
        }
    }
    public static void main(String[] args) throws Exception {

    }
}

找到class文件的目录

在cmd输入javap -c Producer.class 反编译

可以看到同步锁上面monitorenter, 下面monitorexit

就是监视器,和监视器退出;

使用了synchronized 就是monitorenter和monitorexit对应者;

下面还有一个monitorexit是对应,如果在锁里面出现了异常,那么监视器退出

你看第24行就出现了一个athrow

所以一个monitorenter对应2个monitorexit

但是在极端的情况下,一个monitorenter对应一个monitorexit

我们来看下下面的情况

在这种情况下就只有1个monitorenter对应1个monitorexit了

我们在使用javap -v Producer.class 来看下

 public synchronized void aa(){
        System.out.println("同步代码块");
    }

在方法上面加了同步锁之后,就会显示ACC_SYNCHRONIZED,表示是一个同步方法

我们在来看下加了静态同步锁的区别

 public synchronized void aa(){
        System.out.println("同步代码");
    }
    public static synchronized void bb(){
        System.out.println("静态同步代码");
    }

可以看到同步锁前面多了一个ACC_STATIC,那么jvm就知道了那个是静态同步锁方法

为什么任何一个对象都可以成为一个锁?

管程就是monitors,也叫监视器;

每一个对象天生都带着一个对象监视器;

每一个被锁住的的对象都会和monitor关联起来;

什么是公平锁,什么是非公平锁?

公平锁,就是多个人干活,大家都能领取到任务;

非公平锁,就是一个人把所有人的活都干了;

我们来看下代码


public class Producer {

    //默认非公平锁
    ReentrantLock lock=new ReentrantLock();
    int num=50;

    public void aa(){
        //加锁
        lock.lock();
        try {
            if(num>0){
                System.out.println("当前线程:"+Thread.currentThread().getName()+",剩余票数"+(num--));
            }
        }finally {
            //释放锁
            lock.unlock();
        }

    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        new Thread(()->{
            for (int i = 0; i <51 ; i++) {
                producer.aa();
            }
        },"a").start();

        new Thread(()->{
            for (int i = 0; i <51 ; i++) {
                producer.aa();
            }
        },"b").start();

        new Thread(()->{
            for (int i = 0; i <51 ; i++) {
                producer.aa();
            }
        },"c").start();
    }
}

可以看到,线程a一个人把所有票都抢光了

我们在看下公平锁new ReentrantLock(true);设置为true

可以看到a,b,c三个线程都能买到票

为啥默认是非公平锁呢?

因为非公平锁不需要线程切换的开销,减少cpu空闲时间,要比公平锁的速度快,所以默认设置为非公平锁

什么时候使用公平锁,什么时候使用非公平锁?

如果是为了快速处理完一些程序,就是我一个人比那个2个人干活的速度还快,那么使用非公平锁,不用切换线程开销;

否则使用公平锁;

什么是可重入锁?

ReentrantLock和synchronized都是可重入锁;

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入;

自己可以获取自己的内部锁;

就是我大门的锁和屋子里面的锁是同一把,我一个钥匙就可以开2把锁;

我们来看下同步代码块的可重入锁

public class Producer {

    Object o=new Object();

    public void aa(){
        new Thread(()->{
            synchronized (o){
                System.out.println("外层锁"+Thread.currentThread().getName());
                synchronized (o){
                    System.out.println("中层锁"+Thread.currentThread().getName());
                    synchronized (o){
                        System.out.println("内层锁"+Thread.currentThread().getName());
                    }
                }
            }
        },"aa").start();
    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        producer.aa();
    }
}

synchronized默认是可重入锁

在一个被synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码快时,是永远可以得到锁的;

可以看到,3个锁都是同一个线程,都能访问到锁;

可以重入锁指的是可重复递归调用的锁,在外层使用锁之后,在内存仍然可以使用,并且不会发送死锁;

我们来看下加在方法上面的可重入锁

public class Producer {

    public synchronized void aa(){
        System.out.println("我是aa开始"+Thread.currentThread().getName());
        bb();
        System.out.println("我是aa结束"+Thread.currentThread().getName());
    }
    public synchronized void bb(){
        System.out.println("我是bb开始"+Thread.currentThread().getName());
        cc();
        System.out.println("我是bb结束"+Thread.currentThread().getName());
    }
    public synchronized void cc(){
        System.out.println("我是cc开始"+Thread.currentThread().getName());
        System.out.println("我是cc结束"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws Exception {
        Producer producer=new Producer();
        new Thread(()->{
            producer.aa();
        },"t1").start();
    }
}

可以看到并没有发送死锁,拿的也是同一把锁,并且也达到了可重入的效果

接下来我们在看下ReentrantLock是否可以重入锁

 public static void main(String[] args) throws Exception {
        Lock lock=new ReentrantLock();
        new Thread(()->{
           //加锁
           lock.lock();
           try {
               System.out.println("aa:"+Thread.currentThread().getName());
               lock.lock();
               try {
                   System.out.println("bb:"+Thread.currentThread().getName());
               }finally {
                   //释放锁
                   lock.unlock();
               }
           }finally {
               //释放锁
               lock.unlock();
           }
        },"t1").start();
    }

可以看到已经实现了可以重入的效果

当我们使用lock和unlock的时候必须一一匹配,否则会造成死锁;

可重入锁的原理

每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针;

计数器默认为0,当有新的锁对象进来了,并且是当前线程,那么计数器加1,否则等待,直到线程释放该锁;

当jvm执行退出的时候,计数器减1,计数器为0的时候,表示锁已经释放;

接下来我们来看下死锁

public class Producer {


    public static void main(String[] args) throws Exception {
        Object a=new Object();
        Object b=new Object();
        new Thread(()->{
            synchronized (a){
                System.out.println("我是aa1:"+Thread.currentThread().getName());
                synchronized (b){
                    System.out.println("我是bb1:"+Thread.currentThread().getName());
                }
            }
        },"t1").start();

        new Thread(()->{
            synchronized (b){
                System.out.println("我是bb2:"+Thread.currentThread().getName());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println("我是aa2:"+Thread.currentThread().getName());
                }
            }
        },"t2").start();

    }
}

可以看到这个灯一直没有灭

产生死锁的原因?

系统资源不足;

两个线程访问的顺序不合适;

资源分配不当;

我们通过jvm的方式来排查下是否死锁

Found 1 deadlock. 找到一个死锁

t2等待锁xxx4900

t1 锁住xxx4900

我们还可以通过jconsole的命令来查看是否死锁

什么是线程中断机制?

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运;

interrupt :设置线程的中断状态为true,发起一个协商而不会立刻停止线程;

interupted :判断线程是否被中断并清楚当前中断状态;

这方法做了两件事;

1.返回当前线程的中断状态,测试当前线程是否已被中断;

  1. 将当前线程的中断状态清零并重新设置为false,清楚线程的中断状态;

isInterrupted: 测试此线程是否已被中断(通过检查中断标志位);

如何停止中断运行中的线程?

接下来我们来看下,使用volatile来中断线程

public class Producer {

    //可见性 当前线程修改之后 可以被其他线程里面感知到
    private static volatile  boolean flag=false;

    public static void main(String[] args) throws Exception {
        new Thread(()->{
            while (true){
                if(flag){
                    System.out.println(Thread.currentThread().getName()+"已停止");
                    break;
                }
                System.out.println("你好");
            }
        },"t1").start();
        //阻塞一下
        Thread.sleep(100);
        new Thread(()->{
            flag=true;
        },"t2").start();


    }
}

当前t1线程感知到t2线程做出的改变,那么立马停止t1线程

我们还可以通过AtomicBoolean 原子类来实现 中断线程


public class Producer {

    //原子boolean类 默认为false
    private static AtomicBoolean atomicBoolean=new AtomicBoolean(false);

    public static void main(String[] args) throws Exception {
        new Thread(()->{
            while (true){
                if(atomicBoolean.get()){
                    //如果为true 停止循环
                    System.out.println(Thread.currentThread().getName()+"已停止");
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("你好");
            }
        },"t1").start();
        //阻塞一下
        Thread.sleep(100);
        new Thread(()->{
            //设置原子类为true
            atomicBoolean.set(true);
        },"t2").start();


    }
}

我们还可以使用线程自带的api来中断线程

使用interrupt设置为true;

使用isInterrupted判断是否为true

但是在t1线程中千万不要写阻塞的代码,否则就会报错;

public static void main(String[] args) throws Exception {
        Thread t1= new Thread(()->{
         while (true){
                //判断中断线程标志位 是否设置为true ,如果为true 则中断线程
                if(Thread.currentThread().isInterrupted()){
                    //如果为true 停止循环
                    System.out.println(Thread.currentThread().getName()+"已停止");
                    break;
                }
                System.out.println("你好");
            }
        },"t1");
        t1.start();
        //阻塞一下
        Thread.sleep(100);
        new Thread(()->{
            //协商t1线程中断  设置为true
            t1.interrupt();
        },"t2").start();


    }

如果我们的isInterrupted在线程已经操作完了在去执行他,那么就会变成false

我们来看下例子

public class Producer {

    public static void main(String[] args) throws Exception {
       Thread t1 =new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(""+i);
            }
            System.out.println("t1线程:"+Thread.currentThread().isInterrupted());
        },"t1");
       t1.start();

        new Thread(()->{
            t1.interrupt();
            System.out.println("t2线程:"+Thread.currentThread().isInterrupted());
        },"t2").start();

        //这个时候线程都执行完了 所以isInterrupted方法不会受到影响 所以为false
        Thread.sleep(2000);
        System.out.println("main:"+Thread.currentThread().isInterrupted());
    }
}

我们对t1线程加一些阻塞看看什么效果?

public class Producer {

    public static void main(String[] args) throws Exception {
       Thread t1 =new Thread(()->{
            while (true){
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("t1线程已停止");
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("你好");
            }


       },"t1");
       t1.start();

        Thread.sleep(20);
        new Thread(()->{
            t1.interrupt();
        },"t2").start();

    }
}

可以看到报错了 sleep interrupted 睡眠中断

意思是当前你使用了interrupt方法,然后又使用了sleep,那么方法失效

抛出睡眠中断的异常,红灯没有关闭,所以这里不能加阻塞的代码

上面的代码,我们还可以在catch加一句代码 来解决

Thread.currentThread().interrupt();

可以看到,虽然还是报错,但是已经把红灯给停止了

我们在来看看interrupted方法

public static void main(String[] args) throws Exception {
        System.out.println(Thread.interrupted());
        System.out.println(Thread.interrupted());
        System.out.println("-------------");
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted());
        System.out.println(Thread.interrupted());
    }

当我设置了interrupt,为啥最后一个interrupted变成了false呢?

因为当你调用interrupted的时候,先返回了标志位,然后再吧他自己设置为了false

我们来看下源码

在这里测试线程是否已经中断,如果传过来的是true,那么重置,也就是在底层设置为false

接下来我们在来看下LockSupport

LockSupport是什么?

用于创建锁和其他同步类的基本线程阻塞原语

就是阻塞和解除阻塞的意思

我们来看下代码

public class Producer {

    public static void main(String[] args) throws Exception {
        Object o=new Object();
        new Thread(()->{
            synchronized (o){
                System.out.println("进入"+Thread.currentThread().getName());
                try {
                    //阻塞
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"被唤醒");
            }
       },"t1").start();
        
        Thread.sleep(1);
        new Thread(()->{
            //唤醒阻塞的线程
            synchronized (o){
                o.notify();
            }
            System.out.println(Thread.currentThread().getName()+"发出通知");
        },"t2").start();
    }
}

wait必须放在notify的前面;

不能先执行notify,否则报错;

wait和notify必须放在同步锁里面,并且持有同一把对象锁,要不然不知道你释放的是谁的锁

可以看到如果锁的对象不一样,那么t1就无法释放锁,一直在这里阻塞

我们在来看下Condition的阻塞和释放

public class Producer {

    public static void main(String[] args) throws Exception {
        Lock lock=new ReentrantLock();
        //条件
        Condition condition = lock.newCondition();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"进入");
            lock.lock();
            try {
                //阻塞
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"释放锁");
        },"t1").start();

        Thread.sleep(1);
        new Thread(()->{
            lock.lock();
            try {
                //释放锁 唤醒线程
                condition.signal();
            }finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"发出通知");
        },"t2").start();
    }
}

await和signal必须是同一个condition,并且必须要和lock结合使用,否则报错

先await在signal,signal不能在前面,否则报错

接下来我们来看下LockSupport的阻塞和释放锁

在unpark释放锁里面必须要放入,释放哪一个线程,才能释放对方的锁

  public static void main(String[] args) throws Exception {
        Thread t1=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"进入");
            //阻塞
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"释放锁");
        },"t1");
        t1.start();

        Thread.sleep(1);
        Thread t2=new Thread(()->{
            //释放锁
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"发出通知");
        },"t2");
        t2.start();
    }

那么我t1后执行,看看能否释放锁

可以看到顺序错了,也能释放锁,为什么呢?

我们来看下源码

在这个释放锁的方法里,我先给这个线程一个许可证

我们在看这个源码

如果当前线程有许可证,则消耗许可证,并往下走,如果没有许可证,那么阻塞

通过上面的源码分析,我们就可以明白顺序错了,也能往下执行了。

我们的许可证只有1个,并且不会积累;

可以看到下面虽然给了多个许可证,但是只能积累1个,所以在第二个阻塞哪里还会阻塞下去

为什么可以突破wait/notify的原有调用顺序?

因为unpark提前给了许可证,所以在park的时候拿到许可证,就不会阻塞了;

为什么唤醒两次后阻塞多次,但最终的结果还会阻塞线程?

因为unpark的许可证最多只能积累一个,所以还会造成阻塞;

juc并发编程入门(三)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值