美文网首页
多线程与线程安全

多线程与线程安全

作者: 码道功臣 | 来源:发表于2019-05-06 19:01 被阅读0次

多线程核心问题

多线程要解决的核心问题包括三个,分别是原子性问题,可见性问题,有序性问题

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
经典的例子就是银行转账案例。

代码实例1:

i = i + 1;

上述代码最终在CPU中执行的过程:
每个线程在执行上述代码过程中,为了提高CUP性能,会将i值从主存中COPY到CUP的高速缓存中(每个线程运行时有自己的高速缓冲区),当计算完成后,先将计算结果放到高速缓存中,然后再刷新到主存中。
多线程情况下就会出现,由于i值的更新没有同步到其他线程导致计算结果出错的问题。

代码实例2:

x = 10;    //语句1,原子操作
y = x;     //语句2,非原子操作。读取x并写入工作内存 > 将x赋值给y > 将y值更新到工作内存 > 将y值更新到主存
x++;       //语句3,非原子操作。读取x并写入工作内存 > x累加1 > 将x更新到工作内存 > 将x更新到主内存
x = x + 1; //语句4,非原子操作。读取x并写入工作内存 > 将x加1 > 将x更新到工作内存 > 将x更新到主内存

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改这个修改。

对于可见性,Java提供了volatile关键字来保证可见性。

  • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
  • 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性

即程序执行的顺序按照代码的先后顺序执行。

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,进行指令重排序,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
如:

int a = 10;   //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a * a;      //语句4

上面代码经过指令重排后的执行顺序可能是: 语句2 -- 语句1 -- 语句3 -- 语句4 。
在多线程场景下,指令重排也会对执行结果产生影响。

线程的生命周期

图片.png

Java线程具有五中基本状态:

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
  • 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同。
    阻塞状态又可以分为三种:
    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的创建

两种手段,一种是继续Thread类,另外一种是实现Runable接口.(其实准确来讲,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用)
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

多线程控制类及关键字

synchronized

对象同步锁:synchronized是对对象加锁,可作用于对象、方法(相当于对this对象加锁)、静态方法(相当于对Class实例对象加锁,锁住的该类的所有对象)以保证并发环境的线程安全。同一时刻只有一个线程可以获得锁。

其底层实现是通过使用对象监视器Monitor,每个对象都有一个监视器,当线程试图获取Synchronized锁定的对象时,就会去请求对象监视器(Monitor.Enter()方法),如果监视器空闲,则请求成功,会获取执行锁定代码的权利;如果监视器已被其他线程持有,线程进入同步队列等待。

Lock

与synchronized功能类似。

Lock与synchronized的区别:
1、Lock可以通过tryLock()方法非阻塞地获取锁。如果获取了锁即立刻返回true,否则立刻返回false。这个方法还有加上定时等待的重载方法tryLock(long time, TimeUnit unit)方法,在定时期间内,如果获取了锁立刻返回true,否则在定时结束后返回false。在定时等待期间可以被中断,抛出InterruptException异常。而synchronized在获得锁的过程中是不可被中断的。

2、Lock可以通过lockInterrupt()方法可中断的获取锁,与lock()方法不同的是等待时可以响应中断,抛出InterruptException异常。

3、synchronized是隐式的加锁解锁,而Lock必须显示的加锁解锁,而且解锁应放到finnally中,保证一定会被解锁,否则,有可能会产生死锁的问题。而synchronized在出现异常时也会自动解锁,但是对于锁的粒度控制比较粗,同时对于实现一些锁的状态的转移比较困难。但也因为这样,Lock更加灵活。

4、synchronized是JVM层面上的设计,对对象加锁,基于对象监视器。Lock是代码实现的。

可重入锁

ReentrantLock与synchronized都是可重入锁。可重入意味着,获得锁的线程可递归的再次获取锁。当所有锁释放后,其他线程才可以获取锁。

ReentrantLock具有公平和非公平两种模式,也各有优缺点:
公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。

公平锁与非公平锁

“公平性”是指是否等待最久的线程就会获得资源。如果获得锁的顺序是顺序的,那么就是公平的。不公平锁一般效率高于公平锁。ReentrantLock可以通过构造函数参数控制锁是否公平。

ReentrantReadWriteLock(读写锁)

是一种非排它锁, 一般的锁都是排他锁,就是同一时刻只有一个线程可以访问,比如synchronized和Lock。读写锁就多个线程可以同时获取读锁读资源,当有写操作的时候,获取写锁,写操作时,其他读写操作都将被阻塞,直到写锁释放。读写锁适合写操作较多的场景,效率较高。

乐观锁与悲观锁

在Java中的实际应用类并不多,大多用在数据库锁上。

死锁

是当两个线程互相等待获取对方的对象监视器时就会发生死锁。一旦出现死锁,整个程序既不会出现异常也不会有提示,但所有线程都处于阻塞状态。死锁一般出现于多个同步监视器的情况。

BlockingQueue

阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管道,特别适用于先进先出策略的一些应用场景。
除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。
常见的阻塞队列有:ArrayListBlockingQueue、LinkedListBlockingQueue、DelayQueue、SynchronousQueue

ConcurrentHashMap

ConcurrentHashMap从JDK1.5开始随java.util.concurrent包一起引入JDK中,主要为了解决HashMap线程不安全和Hashtable效率不高的问题。众所周知,HashMap在多线程编程中是线程不安全的,而Hashtable由于使用了synchronized修饰方法而导致执行效率不高;因此,在concurrent包中,实现了ConcurrentHashMap以使在多线程编程中可以使用一个高性能的线程安全HashMap方案。
JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap。

ThreadPoolExecutor

ExecutorService e = Executors.newCachedThreadPool();
ExecutorService e = Executors.newSingleThreadExecutor();
ExecutorService e = Executors.newFixedThreadPool(3);
// 第一种是可变大小线程池,按照任务数来分配线程,
// 第二种是单线程池,相当于FixedThreadPool(1)
// 第三种是固定大小线程池。
// 然后运行
e.execute(new MyRunnableImpl());

该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,本质上,他们都是ThreadPoolExecutor类的各种实现版本。

volatile

被volatile修饰的变量:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 添加“内存栅栏”,禁止进行指令重排序。

volatile关键字描述后的代码会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏)。

内存屏障的功能:

  • 保证指令重排序的正确性(volatile 变量代码禁止指令重排序);
  • 强制将缓存的修改立即同步到主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行失效。

volatile相当于轻量级的synchronized,synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

相关文章

  • 2018-05-08

    多线程 初级概念与传参 join deatch 原子变量 互斥锁 与 线程安全 线程安全 多线程访问冲突 冲突...

  • 几个小问题

    1、FMDB与多线程 SQLITE默认的线程模式是串行模式, 是线程安全的FMDatabase多线程不安全, 单个...

  • 高并发

    引入:StringBuffer与StringBuider区别前者是多线程安全的,后者是非多线程安全的,但是效率更高...

  • 22.iOS底层学习之多线程原理

    本篇提纲:1、线程与进程2、多线程3、多线程相关面试题4、线程安全问题5、线程与runloop的关系 线程与进程 ...

  • Android下多线程的实现

    Android下多线程相关 线程安全相关问题参考:java内存模型与线程 android下与多线程有关的主要有以下...

  • JAVA 多线程与锁

    JAVA 多线程与锁 线程与线程池 线程安全可能出现的场景 共享变量资源多线程间的操作。 依赖时序的操作。 不同数...

  • Java一多线程

    目录: 一、进程与线程的概念 二、多线程的概念 三、多线程所存在的问题(线程安全问题、上下文切换) 四、多线程的三...

  • 进程与线程、线程池

    进程与线程的相关总结进程与线程的简单解释进程: 基本的资源分配资源线程: 最小调度单元 线程安全 线程安全是多线程...

  • FMDB与多线程的研究

    前言SQLITE线程安全, 与FMDB多线程安全是两回事;SQLITE默认的线程模式是串行模式, 是线程安全的FM...

  • 线程安全知多少

    1. 如何定义线程安全 线程安全,拆开来看: 线程:指多线程的应用场景下。 安全:指数据安全。 多线程就不用过多介...

网友评论

      本文标题:多线程与线程安全

      本文链接:https://www.haomeiwen.com/subject/pjsdoqtx.html