Thread安全
java内存模型与多线程
JMM(Java Memory Model)
- java作为平台无关语言,JLS(java语言规范)定义了一个统一的内存管理模型。JMM屏蔽了底层平台内存管理细节,在多环境中必须解决可见性和有序行问题。
- JMM规定jvm有
主内存(Main Memory)和工作内存(Working Memory)
,主内存即java堆内存,存放程序中所有的类实例、静态数据等变量,是多个线程共享的
,工作内存存放的是该线程从主内存拷贝过来的变量以及访问方法所取得的局部变量,是每个线程私有的
。 - 每个线程对变量的操作都是
先从主内存将其拷贝到工作内存,再对其进行操作
,多个线程之间不能直接互相传递数据通信,只能通过共享变量。 - 可见性问题
- 时序性问题
线程不安全
- 多个线程同时操作一个数据结构的时候产生了相互修改和串行的情况,没有保证数据的一致性。
public class ThreadMainDemo {
public static void main(String[] args){
Count count = new Count();
for (int i = 0; i<5; i++){
ThreadA task = new ThreadA(count);
task.start();
}
try{
Thread.sleep(100l);
}catch (InterruptedException e){
e.fillInStackTrace();
}
System.out.println("最后的num值为: " + count.num);
}
}
class Count{
public int num = 0;
public void add(){
try{
Thread.sleep(5l);
}catch (InterruptedException e){
}
num += 1;
System.out.println(Thread.currentThread().getName() + "- " + num);
}
}
class ThreadA extends Thread{
private Count count;
public ThreadA(Count count){
this.count = count;
}
@Override
public void run() {
count.add();
}
}
运行结果:
Thread-0- 1
Thread-3- 2
Thread-2- 2
Thread-4- 3
Thread-1- 4
最后的num值为: 4
线程安全
- 多线程情况下能保证数据高度一致性和准确性。
当修改代码add()方法为同步synchronized修饰时
class Count{
public int num = 0;
public synchronized void add(){
try{
Thread.sleep(5l);
}catch (InterruptedException e){
}
num += 1;
System.out.println(Thread.currentThread().getName() + "- " + num);
}
}
运行结果:
Thread-0- 1
Thread-4- 2
Thread-2- 3
Thread-3- 4
Thread-1- 5
最后的num值为: 5
- 实现线程安全的方法:
多实例,不用单例模式
- 使用
java.util.concurrent
下面的类库 - 使用
锁机制synchronized、lock方式
死锁
在两段不同的逻辑都在等待对方的锁释放才能继续工作时,会产生死锁,表面现象是程序再也执行不下去了。
死锁示例:
package com.lym.multithread.deadLock;
public class DeadLockMain {
public static void main(String[] args){
CountDeadLock count = new CountDeadLock();
ThreadATest task = new ThreadATest(count);
task.setName("线程A");
task.start();
ThreadBTest taskB = new ThreadBTest(count);
taskB.setName("线程B");
taskB.start();
}
}
class CountDeadLock{
private byte[] lock1 = new byte[1];
private byte[] lock2 = new byte[1];
public int num = 0;
public void add(){
synchronized (lock1){
try{
Thread.sleep(100l);
}catch (InterruptedException e){
}
synchronized (lock2){
num += 1;
}
System.out.println(Thread.currentThread().getName()+ "-" + num);
}
}
public void lockMethod(){
synchronized (lock2){
try {
Thread.sleep(100l);
}catch (InterruptedException e){
}
synchronized (lock1){
num += 1;
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
}
class ThreadATest extends Thread{
private CountDeadLock count;
public ThreadATest (CountDeadLock count){
this.count = count;
}
@Override
public void run() {
count.lockMethod();
}
}
class ThreadBTest extends Thread{
private CountDeadLock count;
public ThreadBTest(CountDeadLock count){
this.count = count;
}
@Override
public void run() {
count.add();
}
}
volatile关键字
- java关键字volatile修饰变量,表示
该变量是易变的、不稳定的
,所以不要试图对该变量使用缓存等优化机制,而应当每次都从内存地址中去读取值
。 使用volatile标记的变量在读取或写入时不需要使用锁
,减少产生死锁的概率。volatile只提供了内存可见性,而没有提供原子性
,在做高并发的安全机制是不可靠的。- volatile与加锁机制的主要区别是:
加锁机制既能保证可见性又能确保原子性,而volatile变量只能确保可见性
。 - volatile的用法:
public volatile static int count = 0;
原子操作:atomic
- atomic不会阻塞线程,是线程安全的加强版vola原子操作,主要用于在高并发环境下的高效程序处理。
- atomic处理类
* 基本类: AtomicInteger、AtomicLong、AtomicBoolean
* 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedReference、AtomicMarkableReference
* 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
* 属性原子修改器(updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater - AtomicInteger的主要方法:
- public final int get() //获取当前的值
- public final int getAndSet(int newValue) //取当前值,并设置新的值
- public final int getAndIncrement() //获取当前的值,并自增
- public final int getAndDecrement() //获取当前的值,并自减
- public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
AtomicInteger示例:
运行结果:public class AtomicMain { public static void main(String[] args){ AtomicInteger ai = new AtomicInteger(0); System.out.println(ai.get()); System.out.println(ai.getAndSet(5)); System.out.println(ai.getAndIncrement()); System.out.println(ai.getAndDecrement()); System.out.println(ai.getAndAdd(10)); System.out.println(ai.get()); } }
0 0 5 6 5 15
- 原子操作atomic的实现原理
- 利用CPU的
比较并交换(CAS:Compare and Swap)和非阻塞算法
。 - CAS通过调用
JNI(Java Native Interface)的代码实现,JNI为java本地调用
,允许java调用其他语言,compareAndSwapInt是借助C来调用CPU底层指令实现的
。
CAS的缺点:ABA问题
:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号
。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。循环时间长开销大
。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。只能保证一个共享变量的原子操作
。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁
,或者把多个共享变量合并成一个共享变量来操作
。
- 利用CPU的
- concurrent包的实现
- 声明共享变量为volatile;
- 使用CAS的原子条件更新来实现线程之间的同步;
- 配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
实现示意图:
单例模式的写法
- 线程不安全
public class SingletonMain {
private static SingletonMain instance;
private SingletonMain(){
}
public static SingletonMain getInstance(){
if (instance == null){
instance = new SingletonMain();
}
return instance;
}
}
- 线程安全,高并发性能不好
class Singleton{
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
- 线程安全,性能较好
class Singleton2{
private static Singleton2 instance;
private static byte[] lock = new byte[0];
private Singleton2(){
}
public static Singleton2 getInstance(){
if (instance == null){
synchronized (lock){
instance = new Singleton2();
}
}
return instance;
}
}
- 线程安全,性能高
class Singleton3{
private static Singleton3 instance;
private static ReentrantLock lock = new ReentrantLock();
private Singleton3(){
}
public static Singleton3 getInstance(){
if (instance == null){
lock.lock();
if (instance == null){
instance = new Singleton3();
}
lock.unlock();
}
return instance;
}
}