java多线程 (二)线程同步

156 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

JMM

内存模型可以理解为在特定的操作协议下, 对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机(jvm)是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)

20.png

线程/工作内存/主内存 三者的关系

  1. 所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问
  2. 每个线程都有一个独立的工作内存,用于存储线程私有的数据
  3. 线程对变量的操作必须在工作内存中进行(线程安全问题的根本原因)

a. 首先要将变量从主内存拷贝到线程的工作内存中, 不允许直接操作主内存中的变量

b. 每个线程操作自己工作内存中的变量副本,操作完成后再将变量写回主内存

c. 多个线程对一个共享变量进行修改时,都是对自己工作内存中的副本进行操作,相互不可见, 所以主内存最后得到的结果是不可预知的

d. 不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成

JMM的三个特征

可见性

在Java中,不同线程拥有各自的私有工作内存,当线程需要修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到该线程的工作内存中(变量副本),当该线程修改其变量副本后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值,

原因: 工作内存和主内存存在同步延迟

解决方案:

  1. volatile: 可以保证内存可见性

① 规定线程每次修改变量副本后立刻同步到主内存中

② 规定线程每次使用变量前,先从主内存中拷贝最新的值到工作内存中,用于保证能看见其它线程对变量修改的最新值

  1. synchronize: 同步锁 

  2. Lock锁 

注意:

volilate可以保证可见性,但是假设多线程同时在做a++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2

public class Test {
    private static boolean flag = true;
    public static void main(String[] args) {
        //创建一个线程并启动
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(flag){
                    //System.out.println("=============");
                }
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
    }
}

有序性

在单线程程序中代码逐行执行,但是在多线程并发时,程序的执行就有可能出现乱序,

多线程执行程序时, 为了提高性能,编译器和处理器常常会自动对指令做重排序,目的是进行相关的优化, 指令重排序使得代码在多线程执行时可能会出现一些问题

解决方案:

  1. volatile: 可以保证有序性

在指令序列中插入内存屏障(一组处理器指令) , 防止指令重排序

  1. synchronize: 同步锁 

  2. Lock锁 

原子性

原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行

解决方案:

  1. synchronize: 同步锁 

  2. CAS 

  3. Lock锁 

AtomicInteger类,原子性Integer类,底层就使用了volatile+CAS来实现,保证了自增操作的原子性。我们直接使用AtomicInteger类来替代n++即可。

 class TestCAS2 {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) {
       for (int i=0;i<10;i++){
           new Thread(new Runnable() {
               @Override
               public void run() {
                   for (int j = 0; j <10000 ; j++) {
                       atomicInteger.incrementAndGet();
                   }
               }
           }).start();
       }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(atomicInteger);
    }
}

总结

  1. volatile可以解决可见性, 有序性, 但是不能解决原子性

  2. synchronize/Lock可以解决可见性, 有序性, 原子性

Lock锁

基于synchronized锁的一些缺点,JDK1.5中推出了新一代的线程同步方式:Lock锁。更强大、更灵活、效率也更高。其核心API如图所示

image-20220403152919851.png

实现线程同步

/**
 * @Classname Test
 * @Description
 * @Date 2022/4/4 10:24
 * @Author Lee
 */
public class Test {
    public static void main(String[] args) {
        new Thread(new MyTicket(),"窗口A").start();
        new Thread(new MyTicket(),"窗口B").start();
    }
}
​
class MyTicket implements Runnable{
​
    static int num =1;
    static Lock lock = new ReentrantLock(); //设置静态 线程共享
​
    @Override
    public void run() {
​
            while (num<=100){
                lock.lock(); //加锁
                try {
                    if(num<=100) {
                        System.out.println(Thread.currentThread().getName() + "卖出了" + num + "张票");
                        num++;
                    }else {
                        System.out.println("卖完了");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//解锁
                }
            }
​
​
    }
}
​

Lock接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();//试获取锁,成功返回true 失败false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//拿不到锁时会等待一定的时间
    void unlock();
    Condition newCondition();
} 

Lock避免死锁

/**
 * @Classname TestB
 * @Description 解决死锁
 * @Date 2022/4/4 13:04
 * @Author Lee
 */
public class TestB {
    public static void main(String[] args) {
        ReentrantLock ykq = new ReentrantLock();
        ReentrantLock dc = new ReentrantLock();
        new Thread(new XiaobaiRunn(ykq,dc)).start();
        new Thread(new XiaomingRunn(ykq,dc)).start();
    }
}
​
​
class XiaomingRunn implements Runnable{
    private ReentrantLock ykq;
    private ReentrantLock dc;
    public  XiaomingRunn(ReentrantLock ykq,ReentrantLock dc){
        this.ykq = ykq;
        this.dc = dc;
    }
    @Override
    public void run() {
        ykq.lock();
            System.out.println("小明抢到了遥控器,正在准备抢电池");
            if(ykq.isLocked()){
                ykq.unlock();
            }
            dc.lock();
                System.out.println("小明抢到了电池,打开空调爽歪歪");
            dc.unlock();
        if(ykq.isLocked()){
            ykq.unlock();
        }
    }
}
class XiaobaiRunn extends Thread{
    private ReentrantLock ykq;
    private ReentrantLock dc;
    public  XiaobaiRunn(ReentrantLock ykq,ReentrantLock dc){
        this.ykq = ykq;
        this.dc = dc;
    }
    @Override
    public void run() {
        dc.lock();
            System.out.println("小白抢到了电池,正在准备抢遥控器");
         ykq.lock();
                System.out.println("小白抢到了遥控器,打开空调爽歪歪");
         ykq.unlock();
        dc.unlock();
    }
}
​

ReadWriteLock 实现读写操作

/**
 * @Classname TestC
 * @Description 读共享 读写互斥 写互斥
 * @Date 2022/4/4 14:39
 * @Author Lee
 */
public class TestC {
    public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        op op = new op(readWriteLock);
        new Thread(()->op.read(), "A").start();
        new Thread(()->op.read(), "B").start();
        new Thread(()->op.write(), "C").start();
        new Thread(op::write, "D").start();
​
    }
}
​
class op{
    private ReadWriteLock readWriteLock;
    public op(ReadWriteLock l){
        readWriteLock = l;
    }
    public void read(){
        readWriteLock.readLock().lock();
        System.out.println(Thread.currentThread().getName()+"开始读");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束读");
        readWriteLock.readLock().unlock();
​
    }
​
    public void write(){
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName()+"开始写入");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束写入");
        readWriteLock.writeLock().unlock();
​
    }
}

Lock锁和同步锁(synchronized)的选择

  1. 类型不同

synchronized是关键字。修饰方法, 修饰代码块

Lock是接口

  1. 加锁和解锁机制同步

synchronized是自动加锁和解锁,程序员不需要控制。

Lock必须由程序员控制加锁和解锁过程, 解锁时, 需要注意出现异常不会自动解锁

  1. 异常机制

synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。

Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。

  1. Lock功能更强大

Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字,所以无法判断。

  1. Lock性能更优

如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。

  1. 锁住内容不同

synchronized可以锁方法、可以锁代码块

Lock只可以锁代码块