多线程
为了提高CPU的使用率,cpu在同一时刻执行多个任务。在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中,java采用多线程的方式去同时完成几件事情而不互相干扰。
要了解多线程,首先要了解串行和并行的概念,这样才能更好地理解多线程。
串行
串行其实是相对于单条线程来执行多个任务来说的,就拿下载文件来举个例子:当下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A文件之后才能开始下载B文件,它们在时间上是不可能发生重叠的。
并行
下载多个文件,开启多条线程,多个文件同时进行下载,严格意义上的,在同一时刻发生的,并行在时间上是重叠的。(即同一时刻下载A、B文件。)
什么是线程安全?
既然是线程安全问题,那么肯定是在多个线程访问的情况下产生的,没有按照我们预期的行为执行,那么线程就不安全了。也就是说我们想要确保在多线程访问的时候,我们的程序还能按照我们预期的行为去执行,那么就是线程安全。
/**
* @Author 安仔夏天勤奋
* Create Date is 2019/3/28
* Des
*/
public class TestThread {
private static class XRunnable implements Runnable{
private int count;
@Override
public void run() {
for(int i=0;i<5;i++){
getCount();
}
}
private void getCount(){
count++;
//打印 计数值
System.out.println(""+count);
}
}
public static void main(String []arg){
XRunnable runnable = new XRunnable();
Thread a_thread = new Thread(runnable);
Thread b_thread = new Thread(runnable);
Thread c_thread = new Thread(runnable);
a_thread.start();
b_thread.start();
c_thread.start();
}
}
打印出的结果是
1
2
2
4
5
6
7
8
9
10
11
12
13
14
15
Process finished with exit code 0
从代码上看出,启动三个线程,每个线程都是循环5次得出顺序是1到15的结果。从结果可以看到,出现了两个2,出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。
如何确保线程安全?
既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?说说常见的几种方式。先上图,后分析。
不可变(final)
在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。final关键字修饰的类或数据不可修改,可靠性最高。如 String类,Integer类。
线程封闭
多线程访问共享数据为了安全性通常需要同步,如果仅在单线程内访问数据就不需要同步,这种避免共享数据的技术称为线程封闭。线程封闭有三种:
Ad-hoc 线程封闭
Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。
ThreadLocal线程封闭
它是一个特别好的封闭方法,其实ThreadLocal内部维护了一个map,map的key是每个线程的名称,而map的value就是我们要封闭的对象。ThreadLocal提供了get、set、remove方法,每个操作都是基于当前线程的,所以它是线程安全的。
参考 https://blog.csdn.net/forezp/article/details/77620769
堆栈封闭
堆栈封闭其实就是方法中定义局部变量。不存在并发问题。多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型),所以局部变量是不会被多个线程所共享的。
同步
- 悲观锁
- 非阻塞同步(乐观锁)
- 锁优化(过度优化)
悲观锁
同步的最常用的方法是使用锁(Lock),它是一种非强制机制。
每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;
在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
-
synchronized
用来控制线程同步,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性。
注意: 同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。
注意缩小synchronized的使用范围,减少资源浪费。 -
Lock.lock()/Lock.unLock()
ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。
ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
synchronize和ReentrantReadWriteLock都可以保证可见性。
非阻塞同步(乐观锁)
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
- volatile
volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。volatile也可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
工具类
同步容器
工具有Vector、HashTable、Collections.synchroniedXXX()。
并发容器(JUC)
ConcurrentHashMap在jdk1.6中 segment分段锁,读写锁。缺点:弱一致性。
Volative 保证可见性,读写锁。缺点:内存占用,弱一致性
JUC同步器 AQS
总结
总体来说线程安全在三个方面体现
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized)。
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(Java提供volatile来保证一定的有序性,happens-before原则)。
参考链接 https://www.jianshu.com/p/207ac3c11975