Android多线程系列二(线程安全和锁机制)

8 篇文章 1 订阅
2 篇文章 0 订阅

一、volatile

1、volatile起源

概念:在Java内存模型中,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存(共享内存)中进行读写,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。因此就需要把变量声明为volatile。

  • 用volatile关键字修饰的变量对于其他线程来说是立即可见的。
  • 用volatile关键字修饰的变量禁止指令重排。(重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。)。
  • 用volatile关键字修饰的变量无法保证变量的原子性。volatile其实算是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。

2、volatile原理

变量没有添加volatile之前

  • CPU1和CPU2先将count变量读到Cache中,比如内存中count=1,那么现在两个CPU中Cache中的count都等于1
  • CPU1和CPU2分别开启一个线程来对count进行自增操作,每个线程都有一块自己的独立内存空间来存放count的副本。
  • 线程1对副本count进行自增操作后,count=2 ; 线程2对副本count进行自增操作后,count=2
    线程1和线程2将操作后的count写回到Cache缓存的count中
  • CPU1和CPU2将Cache中的count写回内存中的count。
  • 那么问题就来了,线程1和线程2操作的count都是一个count副本,这两个副本的值是一样的,所以进行了两次自增后,写回内存的count=2。而正确的结果应该为count=3。这就是多线程并发所带来的问题。
    在这里插入图片描述

变量添加了volatile之后

  • 当count用volatile关键字修饰后,CPU1对count的值更新后,在写回内存的同时会通知CPU2 count值已经更新了,你需要从内存中获取count最新的值!
    在这里插入图片描述

然而

  • CPU2中的线程如果需要使用到count的时候,就会再从内存中读取count的值来更新自己的Cache。这看上去似乎解决了我们的问题,其实问题依然存在,我们来分析一下:
    比如当前count=1,CPU1和CPU2的Cache中的count都等于1,CPU1中的线程1对count进行了自增操作,然后CPU1更新了内存中count的值,并且通知CPU2 count的值已经改变,然后CPU2从内存中将count=2读到了Cache中,并且线程2开始执行count的自增操作,而就在CPU2刚刚将count的值读回Cache的时候,CPU1又更新了count的值,此时count=3,并且通知CPU2,但是此时线程2已经开始执行了,线程2已经将count=2拷贝到自己的内存空间中了,所以即使CPU2再次更新自己Cache中的count=3,也来不及了,线程2操作的是他自己内存空间中的count副本,所以线程2给count做完自增操作后,将count=3并且写回Cache,CPU2更新内存中的count。此时count的值应该是4,然而CPU2更新完count的值后仍然等于3,这样就出现了错误。我们考虑的是只有两颗CPU的情况,但是现在市面上已经有8核手机了!如果8颗CPU同时工作的话,错误会更严重!因此这个时候就需要引入Atomic原子类和sychronized同步锁了。

二、volatile + Atomic原子类

  • 通过CAS(Compare And Swap)保证其原子性,由于没有阻塞、上下文切换,性能相较于sychronized较高,但存在ABA问题。
public class VolatileAtomicTest {
   private static volatile int num = 0;

   public static void main(String[] args) {
       final CountDownLatch mCountDownLatch = new CountDownLatch(20);
       for (int i = 0; i < 20; i++) {
           new Thread() {
               @Override
               public void run() {
                   super.run();
                   //模拟耗时执行
                   try {
                       Thread.sleep(30);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println("num的值为:" + num++);
                   mCountDownLatch.countDown();
               }
           }.start();
       }
       //主线程await,确保子线程能执行完成
       try {
           mCountDownLatch.await();
           //所有子线程执行完成后,主线程打印
           System.out.println("最终num的值为:" + num);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

------------------------代码输出结果------------------------------
//每次输出的值都不一样,其中一次的结果是:
num的值为:6
num的值为:5
num的值为:5
num的值为:13
num的值为:2
num的值为:0
num的值为:8
num的值为:3
num的值为:4
num的值为:1
num的值为:7
num的值为:12
num的值为:11
num的值为:10
num的值为:9
num的值为:14
num的值为:18
num的值为:17
num的值为:16
num的值为:15
最终num的值为:19

public class VolatileAtomicTest {
    private static volatile AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) {
        final CountDownLatch mCountDownLatch = new CountDownLatch(20);
        for (int i = 0; i < 20; i++) {
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    //模拟耗时执行
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("num的值为:" + num.getAndIncrement());
                    mCountDownLatch.countDown();
                }
            }.start();
        }
        //主线程await,确保子线程能执行完成
        try {
            mCountDownLatch.await();
            //所有子线程执行完成后,主线程打印
            System.out.println("最终num的值为:" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

------------------------代码输出结果------------------------------
num的值为:1
num的值为:0
num的值为:3
num的值为:2
num的值为:5
num的值为:6
num的值为:4
num的值为:8
num的值为:9
num的值为:7
num的值为:10
num的值为:11
num的值为:14
num的值为:12
num的值为:15
num的值为:13
num的值为:16
num的值为:17
num的值为:18
num的值为:19
最终num的值为:20

三、synchronized

概念: synchronized是一个非公平的,独享的,悲观的,互斥的,可重入的锁;优化以后,是基于JVM内部实现,可以根据竞争激烈程度,从偏向锁–>轻量级锁–>重量级锁升级。

  • 公平锁/非公平锁:公平锁遵循FIFO(先入先出原则),先到的线程会优先获取资源,后到的会进行排队等待。非公平锁判断当前锁占用状态==0直接会进行compareAndSetState尝试获取锁,不需要加入等待队列(通过判断hasQueuedPredecessors),然后等待队列头线程被cpu唤醒再获取锁这一步骤,所以效率较快,也不会一直等待被饿死。当然如果获取锁失败,也会进入等待队列。比较典型的如:ReentrantLock提供了两种锁获取方式,FairSyn和NofairSync。
  • 独占锁/共享锁:独占锁又称排他锁,独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁;共享锁放宽了加锁策略,允许多个执行读操作的线程可以同时访问共享资源,JUC中提供了ReentrantReadWriteLock(包含有两把锁ReadLock和WriteLock) ,合称“读写锁”,它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行(线程持有写锁时,读锁会被阻塞,其他想要获取写锁的线程也会被阻塞),因此存在线程饿死问题,后面引入了“公平读写锁”(用队列把请求锁的线程排队)。
  • 悲观锁/乐观锁:悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。独占锁采用悲观保守的加锁策略,是典型的悲观锁(如:ReentrantLock、sychronize);乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。悲观锁常用于多写场景(冲突一般较多),乐观锁则用于多读场景(冲突一般较少)。
  • 互斥锁/自旋锁:互斥锁加锁失败后,线程会释放 CPU ,因此涉及到线程切换的CPU开销;自旋锁加锁失败后,线程会进入忙等待状态,直到它拿到锁,期间不会释放CPU 资源;
  • 不可重入锁/可重入锁:不可重入锁只判断这个锁有没有被锁上,只要被锁上,则其他申请锁的线程都会被要求等待;可重入锁不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一(如:同一线程外层函数获取锁之后,内层递归函数仍然能持有锁继续运行,在一定程度上避免了死锁的发生,因此可重入锁也称递归锁)。synchronized可重入,且加锁和解锁自动进行,不必担心最后是否释放锁。ReentrantLock也可重入,但加锁和解锁需要手动进行,且加锁次数和解锁次数需一样,否则其他线程无法获得锁。
  • 偏向锁/轻量级锁/重量级锁:偏向锁是假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁,该策略主要是考虑频繁的阻塞和唤醒对CPU来说是一件负担),如果明显存在其他线程申请锁(允许短时间的锁竞争),那么偏向锁将很快膨胀为轻量级锁;轻量级锁是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,在拿锁失败的情况,轻量级锁会进行线程自旋,如果自旋失败则会膨胀为重量级锁;重量级锁是有实际竞争,且锁竞争时间长。

1、synchronized的使用

	// 使用 .class 锁(类锁)
	synchronized (SynchronizedObject.class) {
	    xxxxxxxxxx
	}
	
	// 使用当前对象的锁(对象锁)
	synchronized (this) {
	    xxxxxxxxxx
	}
	
	// 使用 Object 对象锁(object对象锁)
	Object c = new  Object ();
	synchronized (c) {
	    xxxxxxxxxx
	}


2、synchronized作用于普通方法代码块

     //普通方法,使用的是调用该方法的对象锁
    synchronized public void method() {}
    private static final byte[] mStaticLockByte = new byte[1];
    private final byte[] mLockByte = new byte[1];

    //普通方法代码块1,使用的是Class类锁
    public void block1() {
        synchronized (SynchronizedObject.class) {}
    }

    //普通方法代码块2,使用的是mLockByte的变量锁
    public void block2() {
        synchronized (mLockByte) {} //变量需要声明为final
    }
    
    //普通方法代码块3,使用的是mStaticLockByte的变量锁
    public void block3() {
        synchronized (mStaticLockByte) {} 
    }

    //普通方法代码块4,使用的是调用该方法的对象锁
    public void block4() {
        synchronized (this) {}
    }

3、synchronized作用于静态方法代码块

    //场景一:直接作用于静态方法。使用的是Class类锁
    synchronized public static void staticMethod() {}


    //场景二:静态方法代码块
   	private static final byte[] mStaticLockByte = new byte[1];

    //静态方法代码块1,使用的是Class类锁
    public static void staticBlock1() {
        synchronized (SynchronizedObject.class) {}
    }

    //静态方法代码块2,使用的是内部静态变量锁
    public static void staticBlock2() {
        synchronized (mStaticLockByte) {} 
    }

四、ReentrantLock

1、ReenTrantLock的简单使用

  1. 区别于Synchronized,需要手动得到锁和释放锁
  2. 粒度更细,可配合condition控制不同的线程
/**
 * 示例一:简单使用ReentrantLock
 */
public class ReentrantLockTest {
    @Test
    public static void main(String[] args) {
        final OutPutter outputter = new OutPutter();
        // 开启一条线程输出名字的每个字符
        new Thread() {
            @Override
            public void run() {
                try {
                    outputter.output("abcde");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        // 开启另一条线程
        new Thread() {
            @Override
            public void run() {
                try {
                    outputter.output("fghij");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    static class OutPutter {
        private Lock lock = new ReentrantLock(); // 定义锁对象

        public void output(String text) throws InterruptedException {
            lock.lock(); // 得到锁
            try {
                for (int i = 0; i < text.length(); i++) {
                    System.out.println(text.charAt(i));
                    Thread.sleep(1000);
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }
    }
}
------------------------代码输出结果------------------------------
a
b
c
d
e
f
g
h
i
j

2、ReenTrantLock配合Condition条件

/**
 * 示例二:配合Condition使用ReentrantLock
 */
public class ThreadDemo3 {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();

        //等待线程A启动
        WaitThread WaitThreadA = new WaitThread(lock, condition1);
        WaitThreadA.start();

        //等待线程B启动
        WaitThread WaitThreadB = new WaitThread(lock, condition2);
        WaitThreadB.start();

        //休眠后100ms后,启动signalA线程,引用condition1
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        SignalThread signalThreadA = new SignalThread(lock, condition1);
        signalThreadA.start();

        //休眠后100ms后,启动signalB线程,引用condition2
        SignalThread signalThreadB = new SignalThread(lock, condition2);
        signalThreadB.start();

    }

    public static class SignalThread extends Thread {
        private final Lock lock;
        private Condition condition;

        public SignalThread(Lock lock, Condition condition) {
            super();
            this.lock = lock;
            this.condition = condition;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("SignalThread启动,开始根据condition唤醒---");
                condition.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public static class WaitThread extends Thread {
        private final Lock lock;
        private Condition condition;

        public WaitThread(Lock lock, Condition condition) {
            super();
            this.lock = lock;
            this.condition = condition;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println(this.getName() + "进入await等待状态---");
                condition.await();
                System.out.println(this.getName() + "进入signal唤醒状态---");
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }

}

------------------------代码输出结果------------------------------
Thread-2进入await等待状态---
Thread-3进入await等待状态---
SignalThread启动,开始根据condition唤醒---
Thread-2进入signal唤醒状态---
SignalThread启动,开始根据condition唤醒---
Thread-3进入signal唤醒状态---

3、CountdownLatch和CyclicBarrier的使用


 /**
 * CountDownLatch用于实施者等待其他线程执行完成,当其他所有人执行完成后,实施者开始执行下一步动作,否则一直处于阻塞态
 */
public class CountDownLatchTest {
    public static void main(String[] args) throws Exception {
        final CountDownLatch countDownLatch = new CountDownLatch(3);
        for (int i = 0; i < countDownLatch.getCount(); i++) {
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    //模拟耗时执行
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("子线程" + Thread.currentThread().getName() + "准备完成");
                    countDownLatch.countDown();
                }
            }.start();
        }
        System.out.println("主线程正在等待其他线程准备好");
        countDownLatch.await();
        System.out.println("主线程开始执行");
    }
}

------------------------代码输出结果------------------------------

主线程正在等待其他线程准备好
子线程Thread-2准备完成
子线程Thread-4准备完成
子线程Thread-3准备完成
主线程开始执行
/**
 * CyclicBarrier用于一个联合任务中,任何一个线程都需要等待其他线程执行完成,才可以执行下一步
 */
public class CyclicBarrierTest {
    public static void main(String[] args) throws Exception {
        int parties = 3;
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(parties, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有人都准备ok了");
            }
        });
        for (int i = 0; i < cyclicBarrier.getParties(); i++) {
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    //模拟耗时执行
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("子线程" + Thread.currentThread().getName() + "准备完成,等待其他线程准备完成---");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println("子线程" + Thread.currentThread().getName() + "执行下一步动作!---");
                }
            }.start();
        }
    }
}

------------------------代码输出结果------------------------------
子线程Thread-2准备完成,等待其他线程准备完成---
子线程Thread-4准备完成,等待其他线程准备完成---
子线程Thread-3准备完成,等待其他线程准备完成---
所有人都准备ok了
子线程Thread-3执行下一步动作!---
子线程Thread-2执行下一步动作!---
子线程Thread-4执行下一步动作!---

五、ReentrantReadWriteLock

1、 ReentrantReadWriteLock的使用

public class ReentrantReadWriteLockTest {
    @Test
    public static void main(String[] args) {
        final Data data = new Data();
        // 开启3个子线程,分别写入数据
        for (int i = 0; i < 2; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 3; j++) {
                        try {
                            data.set(j); // 写入数据
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }

                ;
            }.start();
        }

        // 分别读取数据
        for (int i = 0; i < 2; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 3; j++) {
                        try {
                            data.get(); // 读取数据
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }

                ;
            }.start();
        }
    }

    static class Data {
        private int data;
        private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        public void set(int data) throws Exception {
            readWriteLock.writeLock().lock(); // 获取写锁
            try {
                System.out.println(Thread.currentThread().getName() + "准备写入数据...");
                Thread.sleep(50); // 模拟耗时操作
                this.data = data;
                System.out.println(Thread.currentThread().getName() + "写入数据成功!");
            } finally {
                readWriteLock.writeLock().unlock(); // 释放写锁
            }

        }

        public void get() throws Exception {
            readWriteLock.readLock().lock(); // 获取读锁
            try {
                System.out.println(Thread.currentThread().getName() + "准备读取数据...");
                Thread.sleep(50); // 模拟耗时操作
                System.out.println(Thread.currentThread().getName() + "读取数据:"
                        + this.data);
            } finally {
                readWriteLock.readLock().unlock(); // 释放读锁
            }

        }
    }
}


------------------------代码输出结果------------------------------
Thread-3准备写入数据...
Thread-3写入数据成功!
Thread-3准备写入数据...
Thread-3写入数据成功!
Thread-2准备写入数据...
Thread-2写入数据成功!
Thread-2准备写入数据...
Thread-2写入数据成功!
Thread-4准备读取数据...
Thread-5准备读取数据...
Thread-5读取数据:1
Thread-4读取数据:1
Thread-3准备写入数据...
Thread-3写入数据成功!
Thread-2准备写入数据...
Thread-2写入数据成功!
Thread-4准备读取数据...
Thread-5准备读取数据...
Thread-5读取数据:2
Thread-4读取数据:2
Thread-4准备读取数据...
Thread-5准备读取数据...
Thread-5读取数据:2
Thread-4读取数据:2

六、ThreadLocal

1、ThreadLocal的使用

/**
 * ThreadLocal采用用空间换时间的方式,通过ThreadLocalMap把数据进行线程隔离,
 * 数据不共享,自然也就线程安全了
 */
public class ThreadLocalTest {
    static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            //设置初始化值
            return 0;
        }
    };

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    System.out.println(Thread.currentThread().getName() + "设置前变量的值:" + threadLocal.get());
                    //在每个线程里面,threadLocal每次自增1
                    threadLocal.set(threadLocal.get() + 1);
                    System.out.println(Thread.currentThread().getName() + "设置后变量的值:" + threadLocal.get());
                }
            }.start();
            //休眠一会,保证子线程能够执行完成,主存数据已为最新
            Thread.sleep(1000);
        }


    }
}

------------------------代码输出结果------------------------------
Thread-2设置前变量的值:0
Thread-2设置后变量的值:1
Thread-3设置前变量的值:0
Thread-3设置后变量的值:1
Thread-4设置前变量的值:0
Thread-4设置后变量的值:1

**
 * 与ThreadLocal方式进行比较,发现数据明显无法保证线程隔离
 */
public class ThreadLocalTest2 {
    static int threadLocal = 0;

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    System.out.println("变量的值:" + threadLocal);
                    //在每个线程里面,threadLocal每次自增1
                    threadLocal += 1;
                    System.out.println("设置后变量的值:" + threadLocal);
                }
            }.start();
            //休眠一会,保证子线程能够执行完成,主存数据已为最新
            Thread.sleep(1000);

        }

    }
}

------------------------代码输出结果------------------------------
变量的值:0
设置后变量的值:1
变量的值:1
设置后变量的值:2
变量的值:2
设置后变量的值:3

七、Semaphore

1、Semaphore的使用

/**
 * Semaphore可允许多个线程并发执行,通过设置认证许可来控制并发数量
 */
public class SemaphoreTest {
    public static void main(String[] args) {
        int permits = 3;
        final Semaphore semaphore = new Semaphore(permits);
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        //创建了8个线程,但是最多有3个线程能够并发执行;
        // 每100ms一个线程,则450ms的时间内,如果没有semaphore至少有4个线程可以并发执行
        for (int i = 0; i < 8; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println("线程" + Thread.currentThread().getName() + "已获得许可");
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    try {
                        Thread.sleep(450);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    semaphore.release();
                    System.out.println("线程" + Thread.currentThread().getName() + "已释放许可");

                }
            });

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值