4.线程安全和锁

目录

一、线程安全(Thread Safe)

1.线程不安全出现的原因

1.1没有保证原子性

1.2 内存可见性问题,导致某些线程读取到脏数据

1.3代码重排序导致的问题

1.4一些安全/不安全的线程

2.什么时候不需要考虑线程安全问题

二、synchronize锁-----保护线程安全

1.synchronize修饰方法(普通方法/静态方法)

2.synchronize修饰同步代码块

3.有无static加锁对象的区别

4.synchronized加锁粗粒度的问题

5.synchronize的作用

5.1保证原子性

5.2在有限程度上保证内存可见性

5.3synchronized也可以对代码重排序增加一定的约束

三、Lock锁-----更灵活

 1.Lock中几种常见的方法

1.1lock()方法

1.2lockInterruptibly()方法

1.3tryLock()方法

1.4tryLock(long time, TimeUnit unit)方法

1.5unlock()方法-----释放锁

四、synchronized锁和JUC下的Lock锁的区别

1.synchronized锁

2.Lock锁


一、线程安全(Thread Safe)

代码运行的结果必须是100%符合预期

1.线程不安全出现的原因

1.1没有保证原子性

在开发者眼中:多个线程之间操作同一块数据,至少有一个线程在修改该数据

在系统眼中:Java代码中的一条语句肯对应的是多条指令

线程调度是可能在任意时刻发生,但是不会切割指令(一条指令只有两种情况,执行完和完全没有执行)

线程调度可能发生在这四个阶段 

但是在我们程序员眼中r++是一个原子性的操作(要么执行完成,要么执行失败),但是实际的执行是保证不了原子性的

线程不安全的常见原因-----原子性被破坏

1.2 内存可见性问题,导致某些线程读取到脏数据

指令的执行速度 >> 内存的读写速度

CPU为了提升数据获取速度,解决读写速度不一样,一般在CPU中设置缓存

JVM规定了JVM内存模型,把一个线程想象成一个CPU

主内存:实际内存的模拟

工作内存:CPU中缓存的模拟

 线程中所有的数据操作需要

1.从主内存加载到工作内存中

2.在工作内存中处理

3.完成最终的处理之后,再把数据同步到主内存中

内存可见性:一个线程对数据的操作,很可能其他线程是无法感受到的,甚至再某些情况下,会被优化成完全看不到的结果 

1.3代码重排序导致的问题

我们写的程序,也是经过中间很多环节优化的结果,并不能保证最终指向的语言和我们写的语句是一模一样的,比如:删除某些步骤/调整某些步骤的执行顺序;但是我们是不知道哪里做了优化

代码重排序:执行的指令和书写的指令不一致

基本原则:JVM要求不管怎么优化,对于单线程的视角,结果不能发生改变;但是由于没有规定多线程环境的情况,导致可能在多线程环境下出现问题

1.4一些安全/不安全的线程

ArrayList、LinkedList、Priority、TreeMap、TreeSet、HashMap、HashSet、StringBuilder都不是线程安全的

 Vector、Stack、Dictionary、StringBuffer都是线程安全的

2.什么时候不需要考虑线程安全问题

多个线程之间没有任何数据共享的情况下,是线程安全的

多个线程之间存在数据共享,但是没有修改(写)操作时,是线程安全的

二、synchronize锁-----保护线程安全

  • 当多个线程都有加锁操作并且申请的是同一把锁时,临界区的代码是互斥进行的
  • 根据加锁的原理,只会有一个线程可以持有锁,其余没有抢到锁的线程就会进入到该锁的阻塞队列,放弃CPU,临界区的代码必须在持有锁的前提下才能执行
  • 互斥的必要条件:线程都有加锁操作,是同一把锁,锁的是同一个对象

1.synchronize修饰方法(普通方法/静态方法)

  • 修饰普通方法 -> 对当前对象加锁
  • 修饰静态方法 -> 对当前的类加锁
synchronize 方法类型  方法名称(...){
...
}

2.synchronize修饰同步代码块

synchronize(){
...
}

public class Main {
    // sync 修饰普通方法
    public synchronized int add() {
        return 0;
    }

    // sync 修饰静态方法
    private synchronized static int sub() {
        return 0;
    }

    // sync 同步代码块
    public void method() {
        Object o = new Object();
        synchronized (o) {

        }
    }

    // sync 同步代码块
    public void method2() {
        synchronized (Main.class) {

        }
    }

}

3.有无static加锁对象的区别

 和static有关的只有一个类加锁

4.synchronized加锁粗粒度的问题

  •  粗略的来说,加锁粒度越细,并发的可能性越高;加锁粒度越粗,临界区代码的执行时间会变长
  • 但是粒度没有说哪个更好,最好值是需要经过工程测量的

5.synchronize的作用

5.1保证原子性

通过正确加锁使需要保证原子性的代码互斥来实现

5.2在有限程度上保证内存可见性

 临界区期间的数据读写是不保证的

1.可能读到的数据再次被别的线程更改之后,就看不到了

2.期间是否有数据同步回主内存

 5.3synchronized也可以对代码重排序增加一定的约束

 

三、Lock锁-----更灵活

 

 将unlock()放在finally中,确保任何时候都可以解锁

 1.Lock中几种常见的方法

 1.1lock()方法

如果锁已被其他线程获取,则进行等待

  • 主线程调用 t.jion()等子线程结束
  • 子线程调用lock.lock()等主线程释放锁,陷入死循环
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            lock.lock();
            System.out.println("子线程进入临界区");     // 是一直不会输出的
                                                      // 主线程持有锁,子线程不能加锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();

        t.join();
    }
}

 1.2lockInterruptibly()方法

能够中断等待获取锁的线程

  • 当一个线程获取了锁之后,是不会被interrupt()方法中断的。调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程
  • lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的
  •  只使用interrupt()方法是没有输出的
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            lock.lock();
            System.out.println("子线程进入临界区");     // 是一直不会输出的
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);

        t.interrupt();      // 尝试让子线程停下来,但实际是不会停止的
        
        t.join();
    }
}
  •  加上lockInterruptibly()方法就可以中断了
  •  在请求锁过程中,由于线程被中止了,导致抛出异常就会执行catch中的输出
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                lock.lockInterruptibly();
                System.out.println("子线程进入临界区");     // 理论上,这句代码永远到达不了
            } catch (InterruptedException e) {
                System.out.println("收到停止信号,停止运行"); // 输出这句话,说明此时子线程停止了
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);

        t.interrupt();      // 尝试让子线程停下来

        t.join();
    }
}

1.3tryLock()方法

如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

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

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = lock.tryLock();
            if (b == true) {
                // 加锁成功了
                System.out.println("加锁成功");
                System.out.println("子线程进入临界区");     // 不能输出
            } else {
                System.out.println("加锁失败"); // 输出加锁失败
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);

        t.interrupt();      // 尝试让子线程停下来

        t.join();
    }
}

1.4tryLock(long time, TimeUnit unit)方法

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true

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

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = false;
            try {
                // 等待5s ,如果拿到锁返回true,拿不到锁返回false
                b = lock.tryLock(5, TimeUnit.SECONDS);
                if (b == true) {
                    // 加锁成功了
                    System.out.println("加锁成功");
                    System.out.println("子线程进入临界区");     // 不会输出
                } else {
                    System.out.println("5s 之后加锁失败");
                }
            } catch (InterruptedException e) {
                System.out.println("被打断了"); // 输出 被打断了
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);

        t.interrupt();      // 尝试让子线程停下来

        t.join();
    }
}

1.5unlock()方法-----释放锁

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

/**
 * @author happy
 */
public class Main {
    private static final Lock lock = new ReentrantLock();

    static class MyThread extends Thread {
        @Override
        public void run() {
            boolean b = false;
            try {
                b = lock.tryLock(5, TimeUnit.SECONDS);
                if (b == true) {
                    // 加锁成功了
                    System.out.println("加锁成功");            // 输出 加锁成功
                    System.out.println("子线程进入临界区");     // 输出 子线程进入临界区
                } else {
                    System.out.println("5s 之后加锁失败");
                }
            } catch (InterruptedException e) {
                System.out.println("被打断了"); 
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        lock.lock();

        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(2);

        // 释放锁,此时子线程可以拿到锁
        lock.unlock();

        t.join();
    }
}

四、synchronized锁和JUC下的Lock锁的区别

1.synchronized锁

  • 不需要人为写释放锁
  • 只有一种类型的锁
  • 只有一直请求锁

2.Lock锁

  • 可能忘记写lock.unlock();导致锁不能释放
  • 书写比较灵活,可以在一个方法中加锁,在另外一个方法中解锁
  • 锁的类型更灵活(公平锁/非公平锁 、 读写锁/独占锁 、 可重入锁/不可重用锁)
  • 加锁策略更灵活(一直请求锁、带中断的请求锁、带超时的请求锁)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学习java的张三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值