Java线程入门(4)-synchronized 到底锁什么?怎么才能线程安全?

线程安全

当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题。java提供了两种方式来加锁,一种是关键字:synchronized,一种是concurrent包下的lock锁。synchronized是java底层支持的,而concurrent包则是jdk实现。这两个锁的内部原理差别非常多,博客仅作线程入门了解学习。synchronized会多阐述一些,lock 锁只是简单一提。

多线程不安全的原因

安全可以理解为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。而不安全对这部分的理解涉及到java 的内存模型,结合JMM看待java 进程运行,多线程不安全问题。
java 内存模型java 内存区域

原子性

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

表现的形式就是:线程不安全的代码部分想象成一个房间,每个线程就是要进入这个房间的人。但是如果这个房间没有锁(机制保证),那么一个人进入房间之后,还没有出来,其他人也都可以进入房间,并且打乱第一个人的正常执行,这就是不具备原子性。

可见性

划分的什么栈、程序计数器是从对变量、对象存放的位置来考虑,而主内存和工作内存是从cpu 执行视角来看的。
在这里插入图片描述
在这里插入图片描述
拿抢票的例子来模拟理解,就是还剩最后一张票的时候,一个人去抢,把资源复制到自己的工作内存,正在修改还没把已经没有票的结果返回给主内存的时候,另一个人看到主内存也发现还有最后一张票,也去抢,就会出现两个人抢到一个票,或者出现负数票的情况。(这个例子涉及到了原子性和可见性)。
可以通过volatile 关键字来解决可见性的问题。 后续博客会重点阐述,一个链接放这里啦!

有序性

强调指令之间没有依赖关系 并且顺序换了之后可能效率更高,就会产生

在单线程情况下,JVM、CPU指令集会对没有依赖关系的代码进行优化,使得其执行顺序改变,这叫做指令重排序
如果我们需要线程间相互协调工作,那么任意一个线程站在自己的视角看代码执行,总是有序的,而站在其它线程的视角来看,总是无序的(指令重排序优化)。
在编译和cpu执行的时候,会发生指令重排序。

在这里插入图片描述

在单例模式的时候,会进一步讲解指令重排序
单例模式的指令重排序
参考原子、可见、有序性、重排序回答

解决多线程不安全

线程同步

并发:多个线程访问同一个对象。还经常会出现线程修改对象的情况,所以需要线程同步。
线程同步本质就是一种等待机制,多个需要次对象的线程进入这个对象的等待池,形成队列。等前一个线程形成使用完了之后,后面的线程再使用。 队列+锁保证线程安全性,加锁机制 使用synchronized 关键字。

synchronized 使用

synchronized 的关键字:保证线程安全用的,同步关键字。

  • 加锁和释放锁是基于对象的,只有对同一个对象进行加锁,才会让线程产生同步互斥的作用。
  • synchronized 是一个隐式的锁,可以通过对比Lock锁来理解。
  • 具有可重入性:同一个线程可以对一个对象锁多次申请。基于重入计数器(这个东西可以暂时先不用管,知道有这个特性就行)来实现。申请获取+1,释放-1。(后面可以看看代码演示)

多个线程间同步互斥(一段代码在任意的一个时间点,只有一个线程执行:加锁,释放锁)
没用同步互斥,产生的效果就是多线程并发和并行的执行。

synchronized修饰方法 同步方法

  • 实例同步方法
    这里的object 默认锁的就是this,当前对象。

  • 静态同步方法
    对当前类对象进行加锁。

synchronized 修饰代码块

synchronized (obj){ // 加锁

} // 释放锁

obj 称为同步监视器,obj可以是任何对象,但我们一般是锁住的是同步操作的对象,也就是那个共享资源。
作用:对一段代码进行加锁,让代码满足三个特性:原子性,可见性,有序性。

synchronized 修饰方法、代码块总结
对于普通同步方法,锁是当前实例对象。 如果有多个实 那么锁对象必然不同无法实现同步。 对于静态同步方法,锁是当前类的Class对象。有多个实例但是锁对象是相同的 可以完成同步。
对于同步方法块,锁是Synchonized括号里配置的对象。对象最好是只有一个的 如当前类的 class
是只有一个的,锁对象相同,也能实现同步。

由于经常看到有人说是基于类对象的加锁,所以在这里简单说一下。
基于类对象的加锁
在java世界里,一切皆对象。从某种意义上来说,java有两种对象:实例对象和Class对象。

Class类存在于JDK的java.lang包中,是一个实实在在的类。它包含了与类有关的信息,每个类的运行时的类型信息就是用Class对象表示的。
在java中用来表示运行时类型信息对应类就是Class类。

  • Class类也是类的一种,与class关键字是不一样的。
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且对象保存在同名.class的文件中(字节码文件),比如创建一个Shapes类,编译shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中。
    Class 类对象参考博客理解
synchronized 加锁后观察线程运行状态

在这里插入图片描述

使用synchronized 带来的问题

性能效率的问题

为了保证线程安全,上面采用将一个大的方法或代码块申明为synchronized
但如果现在涉及到对资源访问和对资源进行修改,显然对资源进行修改才需要加锁,如果两个操作都加锁,显然不高效。

使用锁会带来的问题:

  • 一个线程持有锁会导致其他需要此锁的线程挂起。通过上面的线程图可以看出来,竞争失败的线程不停的在阻塞态与运行态之间切换(涉及到用户态和内核态的切换)
  • 多线程竞争的情况下,加锁,释放锁会导致比较多的上下文切和调度延时,导致性能问题。
  • 同步线程数量越多,性能越低
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级性能倒置,引起性能问题。

其实通俗一点说,我们使用多线程就是为了提升效率,让多个线程并发并行的去执行任务,现在好了,全部用同步锁锁起来,那我还用多线程干什么,我直接一个线程去重复执行任务,不就是一定安全的吗,搞个多线程创建调度浪费的时间空间可远比 一个线程麻烦多了,所以多线程一定要有对应的解决机制,达到性能和安全的两者兼顾

synchronized 产生死锁问题
看到一个很有意思的回答
面试官:“你给我讲一下死锁,我就给你通过面试”。
你:“你给我通过面试,我就给你讲一下死锁”。绝了哈哈哈哈哈
什么叫死锁呢?就是互相已经占有资源不放手,还想要对方的资源。

可重入性

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

代码模拟的是创建20个线程,每一线程执行1000此对共享变量的++操作,在++操作之后又去执行–操作,所以最后共享变量还是0。

public class SafeThread2 {
    private static int COUNT = 0;
    public synchronized static void decrement() {
        COUNT--;
    }
    public synchronized static void increment () {
        COUNT++;
        decrement(); // 不会产生自己把自己锁死的问题  我之前已经拿到了这个对象的锁,在这里调用静态方法,又拿到了一次。
    }

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

        Thread[] threads = new Thread[20];
        for (int i = 0; i < 20; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        increment();//静态方法加锁
                    }
                }
            });
        }
        //尽量同时启动,不让new 线程操作耗时影响
        for (Thread t : threads) {
            t.start();
        }
        //让main 线程等待所有的线程执行完毕
        for (Thread t : threads) {
            t.join();
        }
        System.out.println(COUNT);

    }
}

Lock 锁

是一个显示的锁,是属于JUC包下的并发编程Java.util.concurrent.locks.Lock。Lock 锁是显示锁(手动开锁和关闭锁),只可以用来修饰代码块。Lock 是一个接口,RenntrantLock是它的一个实现类是 (可重入锁 re 可重复 entrance进入 lock 锁)
上面提到了CopyOnWriteArrayList 是一个线程安全的集合,里面就用到了RenntrantLock
在这里插入图片描述
用法示例:
在这里插入图片描述
抢票示例说明lock锁

class TestLock2 implements Runnable{
    private  int tickNumbs = 10;
    //定义Lock锁
    private  final  ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            //显示的定义锁
            lock.lock(); // 加锁
            try{
                //下面放不安全的代码
                if(tickNumbs > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"拿到了"+tickNumbs--+"票");
                }else {
                    break;
                }
            }finally {
                //解锁
                lock.unlock();
            }

        }
    }
}
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2,"1").start();
        new Thread(testLock2,"2").start();
        new Thread(testLock2,"3").start();
    }
}

sleep 会放大问题的发生性

经常看到很多地方都加了sleep ,有人说sleep 会放大问题的发生性。通过代码来演示一下,为什么这么说。

list 是一个线程不安全的集合

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //b
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }

            }).start();
        }
         Thread.sleep(3000);
        System.out.println(list.size());
    }
}

通过这个代码想说明两个问题:

  • 同步锁可以使得list 安全
  • 为什么我加了同步锁,没加sleep 还是会出现打印的元素不是10000

第一个问题: 为什么会出现线程不安全,cpu 调度到线程之后并发并行的执行,有可能存在两个线程都准备add 的时候,发现下标i 的地方都没有元素,然后都往里面添加,然后两个线程被添加到同一个位置,元素覆盖掉了。
第二个问题:为什么我加了同步锁,没加sleep 还是看到不安全的情况。朋友啊,这只是你看到的,其实人家已经安全了,那你怎么会看到这个情况,cpu调度太快了, 其实你确实做到了安全,只是你的main 线程早早都走完啦。(我之前的只是start ,有的线程还没轮到cpu调度,可是main 线程却早早就执行完了,也就是会出现在有些线程还没被调度的时候,main 提前打印了,这当然不行)。奥对了,在这里要补充一下,即使main 线程走完了,也不会影响其他线程的运行,我么可以用terminal 输入 jconsole 来查看线程的运行状态。

一个在本篇博客里面没有重点说明的东西是 JUC 安全类型的集合,指的就是这个 java.util.concurrent
java.util.concurrent.CopyOnWriteArrayList 这个类是一个 ArrayList的 线程安全的类 也就是java 帮我实现了线程的安全。
Callable接口也是JUC 下的。后续会在高阶部分补充学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值