线程安全问题

目录

多线程出bug的原因分析 

synchronized(监视器锁)

各种数据结构的线程安全分析

1)多线程情况下使用顺序表:

2)多线程情况下使用队列(使用阻塞队列):

3)多线程情况下使用哈希表(不推荐使用HashTable)

4)ConcurrentHashMap(推荐使用)

ConcurrentHashMap和HashTable的区别(重点)


​​​​​​​

多线程出bug的原因分析 

通过一个例子来描述bug的产生:

package Thread;

public class ThreadDemo7 {
    public static class counter{
        public int count = 0;
        public void print(){
            System.out.println(count);
        }
    }

    public static void main(String[] args) {
        counter counter = new counter();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0 ; i < 100_0000;i++){
                    counter.count++;
                }
            }
        };
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 100_0000;i++){
                    counter.count++;
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        counter.print();
    }
}

图片可以观察到,当我们用多线程操作同一资源时,结果可能和预期不同,出现bug

首先要清楚线程的工作流程:

        一个线程从内存中读取资源,将其放入CPU的寄存器中,执行操作,执行完毕后将其写回内存当中

bug出现的原因:

  1. 抢占式执行。

  2. 修改同一个资源。

  3. 原子性:指令被分为读取,操作,存入。线程之间的指令可以相互穿插。

  4. 内存可见性:编译器在整体逻辑不变的情况下会对指令进行调整,做出一些更有花的执行方案,提高效率,比如读取内存速度比操作寄存器的速度慢上很多。多线程编程的情况下可能会导致线程1还未写入内存时,线程2读取了旧的数据。

  5. 指令重排序:编译器在确保逻辑不变的情况下,会自动的优化执行的顺序提高效率,在单线程的情况下没事,在多线程的情况下可能改变逻辑顺序。

synchronized(监视器锁)

保证原子性,内存可见性和禁止指令重排序

修饰一个方法或者一个代码块

就上一个问题通过synchronized对方法加锁优化后

public class ThreadDemo7 {
    public static class counter{
        public int count = 0;
        synchronized public void increase(){
            count++;
        }
    }

    public static void main(String[] args) {
        counter counter = new counter();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0 ; i < 100_0000;i++){
                    counter.increase();
                }
            }
        };
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 100_0000;i++){
                    counter.increase();
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

 除此之外synchronized还可以通过修饰代码块来枷锁  

public void increase(){
            synchronized (this){
                count++;
            }
        }
public void run() {
                for (int i = 0 ; i < 100_0000;i++){
                    synchronized (counter){
                        counter.increase();
                    }
                }
            }

 这两种方法来实现相同的效果,()里面填写加锁的对象。

synchronized相当于在开始执行前先加入LOCK和UNLOCK,特性就是只能存在一个LOCK,其他线程申请LOCK时便会失败进入阻塞状态直到UNLOCK解锁后。将三个指令打包成为一个原子,不可拆分。

当通过synchronized锁上加锁时,在理论上就会触发死锁。

不过synchronized有特殊方法进行优化(可重入锁),即判断执行LOCK指令加锁线程是否和持有锁线程是同一个线程,如果是则计数器++,后面在遇到继续这样判断,当遇到解锁时继续判断是否为同一个线程,如果是则计数器--,直到计数器为0就会判断为真正的解锁,触发UNLOCK指令。

因为synchronized的多种限制编译器的优化,导致了使用了该结构的线程基本告别了高性能,在个别情况下并不适用,这时引入一个新的辅助功能votalie

volatile保证了内存可见性,禁止了指令重排序,但并没有保证原子性,因此在不访问同一资源的情况下,volatile效率将比synchronized高

各种数据结构的线程安全分析

线程不安全的结构:ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet...

线程安全的结构:Stack,Vactor,HashTable(不推荐使用,简单粗暴一个synchronized完成的)

1)多线程情况下使用顺序表:

        1.自己加锁

        2.Collections.synchronizedList,相当于在ArrayList等集合类加了一层壳,壳里面试用synchronized来加锁。

        3.CopyOnWriteArrayList,让不同的线程使用不同的变量,并没有加锁。属于写时拷贝,多个线程来读取一份数据,某个线程进行了修改,立刻就给这个线程拷贝一份新的数据。

2)多线程情况下使用队列(使用阻塞队列):

        1.ArrayBlockingQueue

        2.LinkedBlockingQueue

        3.PriorityBlockingQueue

        4.TransferQueue(只包含一个元素的阻塞队列)

3)多线程情况下使用哈希表(不推荐使用HashTable)

        并不推荐使用,单纯的一个synchronized对整个哈希表进了加锁,坏处就是锁冲突会特别容易发生。

4)ConcurrentHashMap(推荐使用)

        1.并不是针对整个对象进行加锁,而是分成很多把锁,每个链表/红黑树进行加锁,只有当多个线程修改到同一个链表/红黑树才会发生锁竞争。

        2.针对读操作直接不加锁,虽说是一个十分大胆的操作不过还好,大部分的场景状态下对读操作的线程安全并没有太高的要求,如果有则更加推荐读写锁的使用。

        3.内部采用大量的CAS操作,提高效率

        4.针对扩容进行了优化,HashTable的扩容特别麻烦,需要将整个表进行一次拷贝,如果轮到了哪个倒霉线程去执行,就需要负责整个扩容的过程,相对比来说,ConcurrentHashMap将扩容的任务分散开了,一次只扩容一点,能够更加平滑的过度。

ConcurrentHashMap和HashTable的区别(重点)

        

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

        底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
        实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值