【java底层】多线程的初步理解与锁机制

进程:当执行一个程序(比如QQ.exe)时,内存为程序分配的资源的基本单位。

线程:调度进程的基本单位。

通俗一点说:当你打开360浏览器,就是进程,当从浏览器打开多个网页,就是多个线程。

 

线程:

1、线程的并行和并发:

现在有两个线程,起名叫:小明和小芳

并行:小明和小芳肩并肩在操场上跑步,即:两个线程在同一时刻都在同步的做一些事情。

并发:小明和小芳一前一后的在同一条跑道上跑步,即:两个线程在同一时间段以队列的形式去做一些事情。

2、线程生命周期:

新建:

   new关键字创建一个线程之后,该线程处于新建状态。JVM为线程分配内存,初始化成员变量值。

就绪:

当线程对象调用了start() 方法之后,该线程处于就绪状态,JVM为线程创建爱你方法栈和程序计数器,等待线程调度器调度。

运行:

就绪状态的线程获得CPU资源,开始运行run() 方法。该线程进入运行状态。

阻塞:

当发生以下情况,线程会进入阻塞状态:

  • 线程调用sleep() 主动放弃锁占用的处理器资源
  • 线程调用了一个阻塞式 IO 方法,在该方法返回前,线程处于阻塞
  • 线程试图获取一个同步锁(同步监视器),但该同步锁正被其它线程所持有。
  • 线程在等待某个通知(notify)
  • 程序调用了线程的 suspend() 方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

 死亡:

线程会以一下3中方式结束,结束后处于死亡状态:

  • run()或call()方法执行完成,线程正常结束。
  • 线程抛出未捕获的Exception或Error。
  • 调用该线程stop() 方法来结束该线程,但容易死锁,不推荐使用。

 

3、线程的4中创建:

继承 Thread 父类:

public class ThreadDemo extends Thread {
    @SneakyThrows
    @Override
    public  void run() {
        for (int i = 0; i <10 ; i++) {
            Thread.sleep(50); //模拟网络延迟
            System.out.println("线程::"+Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        System.out.println("main start");
        ThreadDemo threadDemo = new ThreadDemo();
        ThreadDemo threadDemo2 = new ThreadDemo();
        threadDemo.start();
        threadDemo2.start();
        System.out.println("main  end");
    }

}

打印图示:

 正常单线程打印结果应该是 “main  end”在最后结束,按顺序打印。

但多线程情况就是 “main  end” 都打印完毕,其它两个线程还在随机交互打印。

 

实现 Runnable 接口:

public class RunnableDemo implements Runnable {


    @SneakyThrows
    @Override
    public  void run() {
        for (int i = 0; i <10 ; i++) {
            Thread.sleep(50); //模拟网络延迟
            System.out.println("线程::"+Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        System.out.println("main start");
        RunnableDemo threadDemo = new RunnableDemo();

        //开启两个线程
        Thread thread1 = new Thread(threadDemo);
        Thread thread2 = new Thread(threadDemo);
        thread1.start();
        thread2.start();

        System.out.println("main  end");
    }
}

打印图示:

 

实现 Callable  接口+ FutureTask:

public class CallableDemo implements Callable<String> {

    @Override
    public String call() throws Exception {
        for (int i = 0; i <10 ; i++) {
            Thread.sleep(50); //模拟网络延迟
            System.out.println("线程::"+Thread.currentThread().getName());
        }
        return Thread.currentThread().getName()+":打印结束";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main start");
        CallableDemo threadDemo = new CallableDemo();
        //开启两个线程
        FutureTask<String> futureTask = new FutureTask<>(threadDemo);
        new Thread(futureTask).start();
        
        //拿到异步返回结果
//        System.out.println(futureTask.get());
        System.out.println("main  end");
    }
}

打印图示:

当需要获取异步结果时,则调用  futureTask.get() 方法,因为主main线程需要阻塞等待异步线程的返回结果,所以打印图示:

 

使用线程池:

public class RunnableDemo implements Runnable {
//    创建全局线程池,一般整个系统中只需要创建2、3个
//    创建了一个 存有 10个线程的  线程池
    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

    @SneakyThrows
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            Thread.sleep(50); //模拟网络延迟
            System.out.println("线程::"+Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        System.out.println("main start");
//        executorService.submit() 可以获得返回值,
//        executorService.execute();  没有返回值
        executorService.submit(new RunnableDemo());
        executorService.submit(new RunnableDemo());
        System.out.println("main  end");
    }
}

如图所示:

 

四种的区别:

  • Thread  Runnable 无法得到返回结果,Callable可以得到
  • 前三种都无法控制资源,线程池可以。
  • 由于线程池可控资源,所以稳定,常在项目中使用。

异步编排:

https://www.cnblogs.com/lwh1019/p/12896990.html

 

锁机制:

1、为什么要加锁:

锁机制就是保证当出现多线程(或高并发情况)时,内存中的共享数据(变量)安全性。

2、锁的类型:

锁从宏观上分类,分为悲观锁与乐观锁。

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized。

3、java中的重量级锁和轻量级锁:

重量级锁(悲观锁性质):

性质: 在执行代码中进行加锁(synchronized关键词或创建Lock对象),JVM虚拟机将锁的权限移交给操作系统处理。

优点:线程竞争不使用自旋,不会消耗CPU

缺点:多线程或高并发情况时,只允许一个线程去执行被锁代码块,其它线程都处于阻塞状态。

适用场景:追求吞吐量。写操作比读操作频繁。

 

轻量级锁(乐观锁性质):

性质:原理采用CAS(compare and swap)自旋锁,为要修改的数据加上版本号(version),等将数据修改完赋值时先对比版本号是否一致,如果一致则赋值。

优点:多线程或高并发情况不会阻塞,提高了程序的响应速度。

缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU

适用场景: 追求响应时间,读操作比写操作频繁。

4、重量级锁的应用:

1、synchronized 的使用:

本质:synchronized 本质是拥有一把锁,当大量请求去执行被锁代码块时,谁先抢到锁,谁就可以进行操作,并且有且只有一个线程可以访问,其它全部处于阻塞状态,需等锁释放后才可以再次抢锁。

写法:

 

2、Lock 的使用:

本质:从类关系看出Lock接口是jdk5后新添的来实现锁的功能,其实现类:ReentrantLock、WriteLock、ReadLock。其实还有一个接口ReadWriteLock,读写锁(读读共享、读写独享、写读独享、写写独享)。使用上需要显示的获取锁和释放锁,提高可操作性、可中断的获取获取锁以及可超时的获取锁,默认是非公平的但可以实现公平锁,悲观,独享,互斥,可重入,重量级锁

写法:

class MMT {
    String name;
    Lock lock=null;
    public MMT(Lock lock) {
        this.lock=lock;
    }
    public void update(String name) throws InterruptedException{
//	   lock.lock();
//	   boolean tryLock = lock.tryLock();//尝试获取锁
        //中断只是在当前线程获取锁之前,或者当前线程获取锁的时候被阻塞
//	   lock.lockInterruptibly();
        lock.tryLock(3000, TimeUnit.SECONDS);
        try{
            setName(name);
            System.out.println(Thread.currentThread().getName()+" 变换后的姓名为"+name);
        }finally{
            lock.unlock();
        }
    }

    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        final MMT m = new MMT(lock);
        Thread tt = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程一 开始执行。。。");
                try {
                    m.update("张三");
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。");
                }
                System.out.println("线程一 结束执行。。。");
            }
        },"线程一");

        Thread tt2 = new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println("线程二 开始执行。。。");
                try {
                    m.update("李四");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。");
                }
                System.out.println("线程二 结束执行。。。");
            }
        },"线程二");

        tt.start();
        tt2.start();
        //中断线程
        tt.interrupt();
        try {
            tt.join();
            tt2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

区别:

ReentrantLock:使用上需要显示的获取锁和释放锁,提高可操作性、可中断的获取获取锁以及可超时的获取锁,默认是非公平的但可以实现公平锁,悲观,独享,互斥,可重入,重量级锁。ReentrantReadWriteLock:默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

 synchronized:关键字,隐式的获取锁和释放锁,不具备可中断、可超时,非公平、互斥、悲观、独享、可重入的重量级。

 

应用场景:

   在资源竞争不激烈的情况下,synchronized关键字的性能优与ReentrantLock,相反,ReentrantLock的性能保持常态,优于关键字。

5、轻量级锁的应用:

1、也叫自旋锁,采用CAS(compare and swap)(或compare and exchange)(或compare and set)模式,如图:

 2、CAS中的常见ABA问题:

描述:所谓ABA问题就是:当我获取值为1并进行++操作的同时,有其它线程对右侧1值进行1--操作和1++操作,虽然最后的值还是1,但终究不安全。

解决:常见的解决方式就是加版本号(version),举例:

比如一个库存表的结构:

当我要对10001商品库存进行修改时,应先查询对应version的值,然后对stock进行修改时对比version是否有变化,没有变化则修改,有变化则不允许修改。

 

 

 

 

6、公平锁/非公平锁

 公平锁指多个线程按照申请锁的顺序来依次获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序来获取,有可能后申请锁的线程比先申请锁的线程优先获取到锁,此极大的可能会造成线程饥饿现象,迟迟获取不到锁。由于ReentrantLock是通过AQS来实现线程调度,可以实现公平锁,,但是synchroized是非公平的,无法实现公平锁。

 

 

 

 

 

https://blog.csdn.net/cuichunchi/article/details/88532582

https://blog.csdn.net/weixin_42245930/article/details/88604036

  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值