java之多线程(三)

1.线程不安全因素

1.抢占式执行
2.每个线程操作自己的变量
3.非原子操作
4.内存可见性
5.指令重排序

2.volatile 解决内存可见性和指令重排序

1.volatile可以解决内存可见性和指令重排序的问题,代码在写入volatile修饰变量的时候:
(1)改变线程工作内存中volatile变量副本的值。
(2)将改变后的副本的值从工作内存刷新到主内存。
2.代码在读取volatile1修饰的变量的时候:
(1)从主内存中读取volatile变量的最新值到线程的工作内存中。
(2)从工作内存中读取volatile变量的副本。

学习内存可见性时,我们知道是直接访问工作内存(实际是CPU的寄存器或者CPU的缓存),速度非常快,但是可能出现数据不一致的情况。
加上volatile,强制读写内存,速度是慢,但是数据变的更准确了。

3.内存可见性问题:

import java.time.LocalDateTime;

/**
 * 内存可见性问题
 */
public class ThreadDemo17 {
    
    
    //全局变量(类级别)
    private  static boolean flag=true;
    public static void main(String[] args) {
    
    
        //1.创建子线程1
        System.out.println("线程1开始执行"+ LocalDateTime.now());
        Thread t1=new Thread(()->{
    
    
            while(flag){
    
    
            }
            System.out.println("线程1结束执行!");
        });
        t1.start();
        Thread t2=new Thread(()->{
    
    
            //休眠1s
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("线程2修改 flag=false"+LocalDateTime.now());
            flag=false;
        });
        t2.start();
    }
}

在这里插入图片描述
线程1一直在执行,它并没有感知到全局变量flag的变化,这就是内存可见性问题(因为线程2已经把全局变量修改为另一个值了)。
4.volatile可以解决内存可见性问题和指令重排序问题:
在这里插入图片描述
在这里插入图片描述

3.volatile缺点

1.volatile虽然可以解决内存可见性和指令重排序问题,但是解决不了原子性问题,因此对于 ++ 和 – 操作的线程⾮安全问题依然解决不了,⽐如以下代码:

public class ThreadDemoVolatile {
    
    
    static class Counter{
    
    
        //变量
        private volatile int number=0;

        //循环次数
        private int MAX_COUNT=0;

        public Counter(int MAX_COUNT) {
    
    
            this.MAX_COUNT = MAX_COUNT;
        }

        //++方法
        public void incr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                number++;
            }
        }
        //--方法
        public void decr(){
    
    
            for (int i = 0; i < MAX_COUNT; i++) {
    
    
                number--;
            }
        }

        public int getNumber(){
    
    
            return number;
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter=new Counter(100000);
        Thread  t1=new  Thread(()->{
    
    
            counter.incr();
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
           counter.decr();
        });
        t2.start();

        //等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果是:"+counter.getNumber());
    }
}

在这里插入图片描述
注意事项:volatile并不能解决原子性问题,它只能解决内存可见性问题和指令重排序问题。

推断结论:使用volatile并不能完全解决线程安全问题。

4.锁

1.使用锁是java中解决线程安全最主要的手段。
2.java中的锁最主要的有以下两种:
(1)内置锁:synchronized
(2)可重入锁:Lock(ReentrantLock)

4.1synchronized

synchronized的基本用法有以下3种:

4.1.1修饰静态方法

/**
 * synchronized修饰静态方法
 */
public class ThreadSynchronized {
    
    

    private static int number=0;
    static class Counter{
    
    
        //循环次数
        private static int MAX_COUNT=1000000;

        //++方法
        public synchronized  static void incr(){
    
    
            for (int i = 0; i < MAX_COUNT; i++) {
    
    
                number++;
            }
        }
        //--方法
        public synchronized  static void decr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                number--;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1=new Thread(()->{
    
    
            Counter .incr();
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            Counter.decr();
        });
        t2.start();

        //等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果是:"+number);
    }
}

在这里插入图片描述

4.1.2修饰普通方法

/**
 * synchronized修饰普通方法
 */
public class ThreadSynchronized2 {
    
    
    private static int number=0;

    static class Counter{
    
    
        //循环次数
        private static int MAX_COUNT=1000000;
        //++方法
        public synchronized  void incr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                number++;
            }
        }
        //--方法
        public synchronized  void decr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                number--;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
    
    
            counter.incr();
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            counter.decr();
        });
        t2.start();

        //等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:"+number);
    }
}

在这里插入图片描述

4.1.3修饰代码块

1.在普通方法中,this表示当前对象的实例,有new时可以使用:
在这里插入图片描述
注意事项:使用synchronized时,一定要注意,对于同一个业务的多个线程加锁对象,一定要是同一个对象(加同一把锁)。
在这里插入图片描述

2.在静态方法中使用类对象加锁
在这里插入图片描述
注意事项:syncharonized修饰代码块,代码块在静态方法中时,不能使用this对象。在这里插入图片描述
3.自定义锁对象

/**
 * synchronized修饰代码块
 */
public class ThreadSynchronized3 {
    
    
    private static int number;
    static class Counter{
    
    
        //循环次数
        private static int MAX_COUNT=1000000;
        //自定义锁对象(属性名可以自定义)
        private Object mylock=new Object();

        //++方法
        public void incr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                synchronized (mylock){
    
    
                    number++;
                }
            }
        }
      //--方法
        public void decr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                synchronized (mylock){
    
    
                    number--;
                }
            }
        }

    }
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
    
    
           counter.incr();
        });
        t1.start();
        Thread t2=new Thread(()->{
    
    
          counter.decr();
        });
        t2.start();
        //等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:"+number);
    }
}

在这里插入图片描述

4.1.4总结

synchronized用法有3种:
1.修饰静态方法
2.修饰普通方法
3.修饰代码块
synchronized(对象) { //........ }
(1)this(非静态方法)
(2)xxx.class(静态方法)
(3)自定义类对象(使用最多)
在这里插入图片描述

4.2synchronized特性

4.2.1互斥性(排他性)

1.synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其它线程如果也执行到同一个对象synchronized就会阻塞等待
(1)进入synchronized修饰的代码块,相当于加锁
(2)退出synchronized修饰的代码块,相当于解锁

在这里插入图片描述

synchronized用的锁是存在java对象头里的:
在这里插入图片描述
可以粗略理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的“锁定”状态。如果当前未被锁定,那么可以使用,使用是设置锁状态为占用,如果当前是占用状态,那么其他人无法使用,只能排队等候。

(3)阻塞等待
针对每一把锁,操作系统内部维护了一个等待队列,当这个锁被某个线程占用的时候,其他线程尝试进行加锁,就加不上,就会阻塞等待,已知等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
在这里插入图片描述

注意
a.上一线程解锁之后,下一个线程并不是立即就能获取到锁,而是靠操作系统来“唤醒”,这也是操作系统线程调度的一部分工作。
b.假设有A,B,C三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待,但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是和C重新竞争,并不遵守先来后到的规则。

4.2.2刷新内存

1.synchonized的工作过程:
(1)获得互斥锁
(2)从主内存拷贝变量的最新副本到工作的内存。
(3)执行代码
(4)将更改后的共享变量的值刷新到主内存
(5)释放互斥锁。
所以synchronized也能保证内存可见性。

4.2.3可重入性

syncharonized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

/**
 * synchronized可重入性测试
 */
public class ThreadSynchronized4 {
    
    
    public static void main(String[] args) {
    
    
        synchronized (ThreadSynchronized4.class){
    
    
            System.out.println("当前主线程已经得到了锁");
            synchronized (ThreadSynchronized4.class){
    
    
                System.out.println("当前主线程再次得到了锁");
            }
        }
    }
}

在这里插入图片描述
在这里插入图片描述

4.2.4注意事项

1.加同一把锁
在这里插入图片描述
2.实例类可以使用this,静态类使用xxx.class.

4.3synchronized底层实现和运行原理

synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统的互斥锁Mutex实现的。

4.3.1监视器

1.监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。

⼀个监视器像是⼀个建筑,建筑⾥有⼀个特殊的房间,这个房间同⼀时刻只能被⼀个线程所占有。⼀ 个线程从进⼊该房间到离开该房间,可以全程独占该房间的所有数据。进⼊该建筑叫做进⼊监视器 (entering the monitor),进⼊该房间叫做获得监视器(acquiring the monitor),独⾃占有该房间 叫做拥有监视器(owning the monitor),离开该房间叫做释放监视器(releasing the monitor), 离开该建筑叫做退出监视器(exiting the monitor)。

4.3.2底层实现

实现一个synchronized代码块,来观察一下它在字节码层面是如何实现的?
在这里插入图片描述
将上面编译成字节码之后,得到的结果如下:
在这里插入图片描述

上述结果我们发现,在main方法中多了monitorenter和monitorexit的指令:
(1)monitorenter:表示进入监视器
(2)monitorexit:表示退出监视器
由此可知synchronized是依赖Monitor监视器实现的。

4.3.3执行流程

1.在java中,synchronized是非公平锁,也是可以重入锁

(1)非公平锁:是指线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取锁。
在这里插入图片描述

(2)可重入锁:指的是一个线程获取到锁之后,可以重复得到该锁.

在 HotSpot 虚拟机中,Monitor 底层是由 C++实现的,它的实现对象是ObjectMonitor, ObjectMonitor 结构体的实现如下:
在这里插入图片描述

在上面代码中有几个关键属性:
(1)_count:记录该线程获取锁的次数(也就是前前后后,这个线程一共获取此锁多少次)
(2)_recursions:锁的重入次数。
(3)_owner:The Ower拥有者,是持有该ObjectMonitor(监视器)对象的线程。
(4)_EntryList:EntryList监控集合,存放的是处于阻塞状态的线程队列,当线程执行了wait()方法之后,会进入EntryList队列。
(5)_WaitSet:WaitSet待授权集合,存放的是处于 wait状态的线程队列,当线程执行了wait()方法之后,会进入WaitSet队列。

2.监视器执行流程如下:

1.线程通过 CAS(对⽐并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说 明当前线程已经持有锁,并将 _recursions 重⼊次数的属性 +1。如果获取失败则先通过⾃旋 CAS 尝试获取锁,如果还是失败则将当前线程放⼊到 EntryList 监控队列(阻塞)。
2.当拥有锁的线程执⾏了 wait ⽅法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线 程放⼊ WaitSet 待授权队列中等待被唤醒.
3. 当调⽤ notify ⽅法时,随机唤醒 WaitSet 队列中的某⼀个线程,当调⽤ notifyAll 时唤醒所有的 WaitSet 中的线程尝试获取锁.
4.线程执⾏完释放了锁之后,会唤醒EntryList 中的所有线程尝试获取锁。
执行流程图如下:
在这里插入图片描述

3.synchronized在jdk1.6之前使用的比较少,jdk1.6之前synchronized默认是使用重量级锁实现,所以性能比较差。syncharonized1.6优化:
无锁->偏向锁->轻量锁->重量级锁

4.3.4总结

1.synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统互斥锁Mutex实现。JVM监视器的执行流程是:线程先通自旋CAS的方式尝试获取锁,如果获取失败进入EntrySet集合,如果获取成功就拥有该锁。当调用wait()方法时,线程释放锁并进入WaitSet集合,等其他线程调用notify和notifyAll方法时再尝试获取锁。锁使用完释放之后就会通知EntrySet集合中的线程,让他们尝试获取锁。

2.synchronized是如何实现的?(面试必问)

答:JVM层面synchronized是依靠监视器Monitor实现的。
从操作系统层面来看,synchronized是基于操作系统的互斥锁(Mutex)来实现。

4.4Lock

4.4.1Lock基本用法

1.Lock实现步骤:

(1)创建Lock
(2)加锁lock.lock()
(3)释放锁lock.unlock()

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 可重入锁基本使用
 */
public class ThreadLock {
    
    
    public static void main(String[] args) {
    
    
        //1.创建对象
        Lock lock=new ReentrantLock();
        //2.加锁操作
        lock.lock();
        System.out.println("你好 ReentrantLock");
        //3.释放锁
        lock.unlock();
    }
}

在这里插入图片描述
改进后:
在这里插入图片描述
2.利用Lock方法解决线程安全问题:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock2 {
    
    
    private static int number=0;
    //1.创建锁对象
    private static Lock lock=new ReentrantLock();
    static class Counter{
    
    
        //循环次数
        private static int MAX_COUNT=1000000;
        //++方法
        public  static void incr(){
    
    
            for (int i = 0; i < MAX_COUNT; i++) {
    
    
                //2.加锁操作
                lock.lock();
                try{
    
    
                    //业务代码
                    number++;
                }finally{
    
    
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
        //--方法
        public  static void decr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                //2.加锁操作
                lock.lock();
                try{
    
    
                    //业务代码
                    number--;
                }finally{
    
    
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1=new Thread(()->{
    
    
            Counter.incr();
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            Counter.decr();
        });
        t2.start();

        //等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果是:"+number);
    }
}

在这里插入图片描述
在这里插入图片描述

4.4.2 公平锁和非公平锁

1.Lock可以指定锁的类型,默认情况是创建一个非公平锁(性能高),传递参数true会创建一个公平锁。
在这里插入图片描述
2.非公平锁的执行时间:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock2 {
    
    
    private static int number=0;
    //1.创建锁对象
    private static Lock lock=new ReentrantLock();
    static class Counter{
    
    
        //循环次数
        private static int MAX_COUNT=1000000;
        //++方法
        public  static void incr(){
    
    
            for (int i = 0; i < MAX_COUNT; i++) {
    
    
                //2.加锁操作
                lock.lock();
                try{
    
    
                    //业务代码
                    number++;
                }finally{
    
    
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
        //--方法
        public  static void decr(){
    
    
            for (int i = 0; i <MAX_COUNT ; i++) {
    
    
                //2.加锁操作
                lock.lock();
                try{
    
    
                    //业务代码
                    number--;
                }finally{
    
    
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        long stime=System.currentTimeMillis();
        Thread t1=new Thread(()->{
    
    
            Counter.incr();
        });
        t1.start();

        Thread t2=new Thread(()->{
    
    
            Counter.decr();
        });
        t2.start();

        //等待线程执行完成
        t1.join();
        t2.join();
        long etime=System.currentTimeMillis();
        System.out.println("最终的结果是:"+number+"|执行时间:"+(etime-stime));
    }
}

在这里插入图片描述
3.公平锁的执行时间:
在这里插入图片描述
非公平锁执行效率高

4.4.3Lock注意事项-放到try外

1.unlock操作一定要放在finally里面,因为如果不放在finally中可能会导致锁资源永久占用问题。

2.lock()一定要放在try之前或者是try的首行。

如果lock没有放在try之前,会产生两种问题:
(1)未加锁却执行了释放锁操作
当lock操作放在try方法内时:
lock操作如果放在 try 里面,因为 try 代码中的异常导致加锁失败,还会执⾏ finally 释放锁的操作,未加锁而释放锁会产生错误。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock3 {
    
    
    public static void main(String[] args) {
    
    
        Lock lock=new ReentrantLock();
        try{
    
    
            System.out.println("进入了try方法");
            //某一种极端操作
            int i=10/0;
            System.out.println("执行lock操作");
            lock.lock();
        }finally{
    
    
            System.out.println("执行unlock操作");
            lock.unlock();
        }
    }
}

在这里插入图片描述
(2)释放锁的错误信息会覆盖业务报错信息,从而增加了调试程序和修复程序的复杂度。
在这里插入图片描述

4.5synchronized 和 Lock的区别(重点)

1.Lock更灵活,有更多的方法,比如tryLock().
在这里插入图片描述
2.锁类型不同:Lock默认是非公平锁,但可以指定为公平锁,synchronized只能为非公平锁。
3.调用lock方法和synchronized线程等待锁状态不同,lock方法会变为WAITING,synchronized会变成BLOCKED.
(1)调用lock方法:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock4 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    

        Lock lock = new ReentrantLock();
        System.out.println("执行 lock 操作");
        Thread t1 = new Thread(() -> {
    
    
            lock.lock();
            System.out.println("线程1得到了锁");
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                System.out.println("线程1释放锁");
                lock.unlock();
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            lock.lock();
            try {
    
    
                System.out.println("线程2获取到了锁");
            } finally {
    
    
                lock.unlock();
            }
        });
        t2.start();
        Thread.sleep(1500);
        System.out.println("线程2:" + t2.getState());
    }
}

在这里插入图片描述
(2)synchronized

public class ThreadLock4 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            synchronized (ThreadLock4.class) {
    
    
                System.out.println("线程1得到了锁");
                try {
    
    
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("线程2释放锁");
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            synchronized (ThreadLock4.class) {
    
    
                System.out.println("线程2获取到了锁");
            }
        });
        t2.start();

        Thread.sleep(1500);
        System.out.println("线程2:" + t2.getState());
    }
}

在这里插入图片描述
4.synchronized是JVM层面提供的锁,它是自定进行加锁和释放锁的操作,对于开发者是无感的,而Lock需要开发者自己进行加锁和释放锁的操作。
5.syncharonized可以修饰方法(静态方法/普通方法)和代码块,而Lock只能修饰代码。

猜你喜欢

转载自blog.csdn.net/weixin_51970219/article/details/123832501