Android中的线程线程安全 & 线程同步

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android中的线程线程安全 & 线程同步相关的知识,希望对你有一定的参考价值。

线程安全

线程安全又叫线程同步,在Java中多个线程同时访问一个可共享的资源变量时,有可能会导致数据不准确,导致使用该变量的逻辑出现问题,因此使用同步锁来保证一个线程对该资源的操作完成之前,其他线程不会对其进行操作,即保证线程同步安全。

因此线程同步的本质就是线程排队,这可能跟它的字面意思相反,它的目的就是保证线程按照先后顺序一个一个的访问共享资源,为了避免同时操作同一个共享资源。

在这里插入图片描述
在Java中实现线程安全的主要手段有:

  • synchronized 同步锁
  • ReentrantLock 重入锁
  • volatile 线程可见
  • Atomic 原子类家族,如AtomicIntegerAtomicBoolean
  • ThreadLocal 本地副本
  • Semaphore 信号量
  • ReadLock/WriteLock 读写锁
  • Concurrent 线程安全的集合类,如ConcurrentHashMap
  • BlockingQueue 阻塞队列
  • CountDownLatch 计数器

synchronized 同步锁

synchronized关键字为同步锁,可重入锁,可以修饰普通方法、静态方法、代码块,但构造方法和成员变量除外。

同步方法:

public class Foo {
	public synchronized void method() {
  		//...
	}
}

这等价于:

public class Foo {
	public void method() {
	  synchronized(this) {
	  	//...
	  }
	}
}

加在普通方法上的synchronized关键字修饰使用的同步锁的是当前对象,就是调用当前方法的那个对象,注意这里是指同一个对象,假如我有线程A、线程B,Foo的实例obj1, Foo的实例obj2,那么只有线程A和线程B同时调用obj1method()方法(或同时调用obj2的),这时才会导致同步锁竞争,假如线程A访问的是obj1method()方法,而线程B访问的是obj2method()方法,那么它们互不影响,不会产生竞争,能同时访问执行。而访问同一个对象的不同的普通同步方法,也会产生竞争,因为它们是使用同一个this对象,竞争同一个锁。也就是说看是否会产生竞争本质是看使用的锁是否是同一个对象

同步静态方法:

public class Foo {
	public synchronized static void method() {
		//...
	}
}

这等价于:

public class Foo {
	public static void method() {
		synchronized (Foo.class) {
			//...
		}
	}
}

加在静态方法上面的synchronized关键字使用的锁是当前的类对象(Class<Foo>)作为同步锁,可以理解下面的代码:

public class Foo {
	public static void method() {
		Class<Foo> obj = Foo.class;
		synchronized (obj) {
			//...
		}
	}
}

假如有多个线程内同时调用Foo.method()时就会导致竞争,但是假如Foo类内部还有一个普通的非静态同步方法method2(), 那么当不同线程同时访问method()method2() 方法时,它们并不会导致竞争,可以同时执行,因为它们所使用的加锁对象不同,一个是Foo类的实例对象Foo obj,一个是Class类的对象Class<Foo> obj = Foo.class

同步代码块:

public class Foo {
	public static final Object lock = new Object();
   
    public void f1() {
       	synchronized (lock) { // lock 是公用同步锁
        	// 代码段 A
			// 访问共享资源 resource1
			// 需要同步
		}
	}
	public void f2() {
		synchronized (lock) { // lock 是公用同步锁
  			// 代码段 B
			// 访问共享资源 resource1
			// 需要同步
		}
	}
}

前面的普通同步方法和静态同步方法本质上都是同步代码块的实现,synchronize的括号里面的对象可以是任意一个Object对象,这里的 lock 对象并不一定是public static final的,但是必须保证的是,多个线程在访问同一共享资源时使用的是同一个锁对象,即唯一的锁对象,才有意义。

比如,像下面这样写是完全没有意义的:

public class Foo {
    public void f1() {
    	Object lock = new Object();//局部变量作为锁
    	
       	synchronized (lock) { 
        	// 代码段 A
			// 访问共享资源 resource1
			// 需要同步
		}
	}
}

这段代码使用的锁对象是方法内部的局部变量,这样每调用一次该方法就会生成一个新的锁对象,导致最终在不同线程中调用该方法使用的是不同的锁对象,它们之间根本就不会产生竞争,完全达不到锁的目的。除非我们将该对象改成公用的唯一值。

注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是等待队列,就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而等待队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。

成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者继续待在就绪队列中。

线程的等待 & 唤醒机制 wait & notify

synchronized代码块中可以使用wait & notify方法实现线程的等待和唤醒机制,又称线程间的通信,但是我觉得描述为等待和唤醒更加准确,因为线程通信是可以发送携带消息的,而wait & notify只是发一个通知并不能携带额外的信息。

Object obj = new Object();

public void f1() {
	synchronized(obj){
		while(condition) {
			...
			obj.wait(); //进入等待状态,释放同步锁,直到有人唤醒它
			...
		}
	}
}

public void f1() {
	synchronized(obj){
		...
		obj.notify(); //唤醒等待obj的线程,当前线程继续执行
		...
	}
}

方法说明
wait()释放当前线程的同步锁,使当前线程进入等待状态,直到有其他线程调用notify()或notifyAll()唤醒
notify()通知唤醒等待该对象的线程,使该线程从等待状态进入可运行状态,当前线程继续执行,被唤醒的线程不一定立马运行,这取决于当前调用notify的线程是否执行完了synchronized代码块释放了同步锁。如果有多个线程在等待该对象,那么由操作系统指定该执行哪一个。
notifyAll()唤醒所有等待该对象的线程,哪一个线程先执行取决于系统实现

waitsleep的区别:waitnotify只能在synchronized代码块中调用,否则会抛异常,sleep可以在任何地方调用。wait进入等待的线程必须由notify来唤醒,而sleep等待的线程到指定时间会自动唤醒执行。waitsleep都能被中断,响应中断异常。

synchronized实现原理:

每个Java对象可以作为同步锁是通过一个 内置锁 或者 监视器锁(Monitor Lock) 的指令来实现的。

java每个对象都有一个对象头,对象头中有个部分就是用来存储synchronized关键字锁的。
当线程访问一个同步代码块或者方法时必须得到这把锁,当退出或者抛出异常时会释放这把锁,进入锁释放锁是基于monitor对象来实现同步的,monitor对象主要有两个指令monitorenter(插入到同步代码开始位置)、monitorexit(插入到同步代码结束的时候),JVM会保证每个enter后有exit与之对应,但是可能会有多个exit和同一个enter对应,因为退出的时机不仅仅是方法退出也可能是方法抛出异常。每个对象都有一个monitor与之关联,一旦持有之后,就会处于锁定状态,当线程执行到monitorenter这个指令时,就会尝试获取该对象monitor所有权(尝试获取该对象锁)。

monitorenter和monitorexit指令

监视器锁的原理是通过计数器来实现,通过monitorentermonitorexit指令会在执行的时候让锁计数减1或者加1,每个对象都与一个monitor对象关联,一个monitorlock锁只能被一个线程在同一时间获得,一个线程在尝试获得与这个对象关联的monitor所有权时只会发生以下三种情况之一:

  1. 如果这个monitor计数器为0,意味着目前还没有被获得,然后这个线程会立刻获得然后将计数器加1, 其它线程就会知道该monitor已经被持有了,这就是成功获得锁。
  2. 如果这个线程已经拿到了monitor的所有权,又重入了,计数器就会再加1变成2、3…(可重入原理
  3. 如果monitor已经被其它线程持有了,现在去获取就会得到无法获取信号,那么就会进入阻塞状态,直到monitor计数器变为0再去尝试获取锁。

monitorexit就是对锁进行释放,释放的过程就是将计数器减1,如果减完之后计数器变为0就意味着当前线程不再拥有对锁的所有权了。如果减完之后不为0就意味着是重入进来的,那么就继续持有该锁。

monitorentermonitorexit指令可以在反编译的java class文件中对应的同步代码块位置查找到。

总结synchronized注意事项:

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁。
  • 每个对象只有一个锁(lock)与之相关联。
  • Synchronized修饰的方法或代码块执行完毕后,同步锁是自动释放的,但是在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放
  • 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池。
  • 当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。
  • synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不再保持同步,除非用synchronized修饰。
  • 加锁对象不能为空,锁是保存在对象中的,为空自然不行。

synchronized缺陷:

  • 不够灵活,不能手动释放锁,一旦加锁成功就必须一直等待同步代码块全部执行完毕,或者抛出异常。
  • 不能中断正在加锁的线程, 相比于Lock
  • 不能获取申请锁的结果是否成功,相比于Lock
  • 不能做到读写锁分离

ReentrantLock 重入锁

ReentrantLockLock接口的实现类,是同步锁,可重入锁,可重入锁是指支持一个线程内对资源的重复加锁,也就是可以多次调用lock()方法获取锁而不被阻塞,synchronized本身也是可重入锁,只不过ReentrantLocksynchronized控制更灵活更具可操作性,能更加有效的避免死锁。同时ReentrantLock还支持获取锁的公平性和非公平性。

可重入锁的示例:

void methodA(){
    lock.lock(); // 获取锁
    methodB();
    lock.unlock() // 释放锁
}

void methodB(){
    lock.lock(); // 获取锁
    // 其他业务
    lock.unlock();// 释放锁
}

public void doWork(){ 
	lock.lock() 
	doWork();//递归调用,使得统一线程多次获得锁 
	lock.unLock() 
}

ReentrantLock内部采用的是计数功能,当一个线程内调用lock方法时,计数器会设置为1,假如内部某个时刻再次申请时没有其他线程持有该锁,那么计数器加1,每当调用unlock方法时计数器减1。

方法说明
void lock()获取锁
void lockInterruptibly() throws InterruptedException获取锁,并且在获取锁成功后可响应中断操作
boolean tryLock()尝试非阻塞式获取锁,调用该方法后立即返回能否获取到锁,如果能申请返回true, 否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException尝试在超时时间内获取锁,如果超时未获取到返回false,否则返回true, 并且在该时间内支持中断操作
void unlock()释放锁
Condition newCondition()生成绑定到加锁对象的子条件实例,在lock成功的前提下,该类可以通过Condition#await() 或Condition#signal() 来等待或唤醒线程,实现更精细的控制

ReentrantLock 的构造函数为 ReentrantLock(boolean fair), 这里的布尔参数fair是用来设置公平锁还是非公平锁,区别主要取决于申请时间上:

  • 公平锁: 操作会排一个队按顺序执行,来保证执行顺序。所有进入阻塞的线程排队依次均有机会执行。

  • 不公平锁: 是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。避免每一个线程都进入阻塞,再唤醒,性能高。因为线程可以插队,导致队列中可能会存在线程饿死的情况,一直得不到锁,一直得不到执行。

ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
ReentrantLock lock = new ReentrantLock(true); //公平锁  
  
lock.lock(); 
try {  
    //操作  
} finally {  
    lock.unlock();  //释放锁
}  

// 等价于synchronized关键字
// public synchronized void method {
//    
// }

jdk推荐将 unlock() 方法写在 finally 当中,这样能确保即便操作中发生异常也能最终释放锁。很显然与synchronized相比,synchronized的优势是加锁和释放锁都是自动处理的,不需要开发者手动操作。

支持响应中断:

ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
     lock.lockInterruptibly();//获取锁, 中断
     //操作
 } catch (InterruptedException e) {
     Thread.currentThread().interrupt();//出现异常就中断
 } finally {
     lock.unlock();//释放锁
 }

防止重复执行(忽略重复触发):

ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 
	try {
		//操作
	} finally {
		lock.unlock();
	}
} else {
	//申请失败操作
}

lock.tryLock()非阻塞的方法,即立马返回能否获取到锁的结果,这样提高了程序执行的性能。

支持超时等待&中断:

ReentrantLock lock = new ReentrantLock(true); //公平锁  
try {  
	//尝试5s内获取锁,如果5s后仍然无法获得锁则返回false继续执行
    if (lock.tryLock(5, TimeUnit.SECONDS)) {      
       try {  
            //操作  
        } finally {  
            lock.unlock();  
        }  
    }  
} catch (InterruptedException e) {  
    e.printStackTrace(); //当前线程5s内被中断(interrupt),会抛InterruptedException                   
}  

newCondition子条件控制:

ReentrantLock lock = new ReentrantLock(); 
Condition worker1 = lock.newCondition(); 
Condition worker2 = lock.newCondition(); 

class Worker1 {
 	..... 
 	worker1.await()//进入阻塞,等待唤醒 
 	..... 
} 

class Worker2 {
 	..... 
 	worker2.await()//进入阻塞,等待唤醒 
 	..... 
} 

class Boss { 
  if(...) { 
	 worker1.signal()//指定唤醒线程1 
  } else { 
     worker2.signal()//指定唤醒线程2 
  } 
}

Condition的作用是对锁进行更精确的控制,可使用它的await-singnal 指定唤醒一个(组)线程。相比于Objectwait-notify要么全部唤醒,要么只能唤醒一个,更加灵活可控。

使用示例:

public class ProducerConsumerTest {

    private Lock lock = new ReentrantLock();

    /** 子条件控制 */
    private Condition addCondition = lock.newCondition();
    private Condition removeCondition = lock.newCondition();

    private LinkedList<Integer> resources = new LinkedList<>();

    private int maxSize;

    public ProducerConsumerTest(int maxSize) {
        this.maxSize = maxSize;
    }

    /** 生产者 */
    public class Producer implements Runnable {

        private int proSize;

        private Producer(int proSize) {
            this.proSize = proSize;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                for (int i = 1; i < proSize; i++) {
                    while (resources.size() >= maxSize) {
                        System.out.println("当前仓库已满,等待消费...");
                        try {
                            // 生产者进入阻塞状态,等待消费者唤醒
                            addCondition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("已经生产产品数: " + i + "\\t现仓储量总量:" + resources.size());
                    resources.add(i);
                    //唤醒消费者
                    removeCondition.signal();
                }
            } finally {
                lock.unlock();
            }

        }
    }

    /** 消费者 */
    public class Consumer implements Runnable {

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            while (true) {
                lock.lock();
                try {
                    while (resources.size() <= 0) {
                        System.out.println(threadName + " 当前仓库没有产品,请稍等...");
                        try {
                            // 消费者进入阻塞状态,等待生产者唤醒
                            removeCondition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 消费数据
                    int size = resources.size();
                    for (int i = 0; i < size; i++) {
                        Integer remove = resources.remove();
                        System.out.println(threadName + " 当前消费产品编号为:" + remove);
                    }
                    // 唤醒生产者
                    addCondition.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public void test() {
        new Thread(new Producer(100), "producer").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(new Consumer(), "consumer").start();
    }
    
    public static void main(String[] args) throws InterruptedException {
        new ProducerConsumerTest(10).test();
    }
    
}

synchronized 相比 ReentrantLock 最大的优势就是能获取申请锁的结果是成功还是失败,并且申请锁的过程中支持中断,加锁以后可以主动退出(unlock)。

volatile 线程可见

volatile关键字修饰的成员变量可以保证在不同线程之间的可见性,即一个线程修改了某个变量的值,新的值对其他线程来说是立即可见的。

CPU高速缓存:

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存


当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中


在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存,如果一个变量是被多个线程同时访问的共享变量,该变量就会同时存在于不同CPU缓存当中,就可能存在缓存不一致的问题
在这里插入图片描述

因此为了解决缓存一致性问题,有两种方案:一种是加同步锁, 另一种是通过缓存一致性协议(MESI)。由于加锁会导致锁住CPU的过程中其他CPU无法访问该内存,性能较差,因此出现了缓存一致性协议方案:

当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

Java内存模型(JMM)规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。因此Java内存当中也存在缓存一致性问题,而volatile关键字就是Java用来解决缓存一致性问题的方案。
在这里插入图片描述
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 可见性:保证在不同线程间可见,当变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,当成员变量值每次发生变化时,又强迫将其变化的值重新写入共享内存。
  2. 有序性:禁止指令重排序。

注意volatile不能保证原子性,即不能解决非原子操作的线程安全。它的性能不及原子类高。

关于原子性可见性有序性以上是关于Android中的线程线程安全 & 线程同步的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 互斥锁&&自旋锁 多线程安全隐患(转载)

线程安全数据同步之 synchronized 与 Lock

第二十四天 多线程-多线程&线程安全悟空教程

什么是线程安全?

Android-Java-单例模式优化&多线程并发

iOS开发多线程篇—线程安全