多线程与线程安全

2019-05-06  本文已影响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关键字来保证可见性。

有序性

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

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

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

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

线程的生命周期

图片.png

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

线程的创建

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

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

上一篇下一篇

猜你喜欢

热点阅读